Compare commits
55 Commits
544ed835b7
...
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 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,5 +1,9 @@
|
||||
/*.o
|
||||
/strans
|
||||
/imv2.c
|
||||
/imv2.h
|
||||
/vkv1.c
|
||||
/vkv1.h
|
||||
/tests/*.o
|
||||
/tests/unit_test
|
||||
/tests/stress_test
|
||||
@@ -14,3 +18,4 @@
|
||||
/gtk/im-strans.so
|
||||
/bench/bench
|
||||
/bench/perf.data*
|
||||
/ref-*/
|
||||
|
||||
@@ -20,6 +20,7 @@ RUN pacman -Syu --noconfirm --needed \
|
||||
ttf-dejavu \
|
||||
ttf-jigmo \
|
||||
valgrind \
|
||||
wayland \
|
||||
which \
|
||||
xorg-server-xvfb \
|
||||
xcb-imdkit \
|
||||
@@ -41,4 +42,9 @@ RUN dbus-uuidgen --ensure=/etc/machine-id
|
||||
ENV PLAN9=/usr/lib/plan9
|
||||
ENV PATH=$PATH:/usr/lib/plan9/bin
|
||||
|
||||
# 9c and 9l filter their own output through $egrep, which plan9port never
|
||||
# assigns, so the pipeline dies and takes every warning and error with it.
|
||||
# Both source $PLAN9/config, which the distribution does not ship.
|
||||
RUN echo 'egrep="grep -E"' > $PLAN9/config
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# License inventory
|
||||
|
||||
- `BSD-3-Clause-libhangul-hanja.txt` contains the terms for the
|
||||
single-character Hanja data in `map/hanja.src` and generated
|
||||
`map/hanja.dict`, derived from libhangul's `data/hanja/hanja.txt`.
|
||||
- `BSD-3-Clause-libhangul-hanja.txt` contains the terms for the Hanja data
|
||||
in `map/hanja.src` and the symbol data in `map/mssymbol.src`, and for the
|
||||
generated `map/hanja.dict` they share, derived from libhangul's
|
||||
`data/hanja/hanja.txt` and `data/hanja/mssymbol.txt`.
|
||||
- `GPL-2.0-or-later.txt` contains GNU GPL version 2. The header of
|
||||
`map/kanji.dict` grants the option to use GPL version 2 or any later
|
||||
version.
|
||||
@@ -10,4 +11,7 @@
|
||||
emoji names and keywords in `map/emoji.src` and generated
|
||||
`map/emoji.dict`, derived from Unicode's `emoji-test.txt` and CLDR's
|
||||
annotations.
|
||||
- The two protocol descriptions in `proto/` are vendored verbatim from
|
||||
wlroots `012ca825` and carry their own MIT terms inside the XML.
|
||||
|
||||
No repository-wide license has been declared for original strans source.
|
||||
|
||||
27
Makefile
27
Makefile
@@ -1,6 +1,7 @@
|
||||
CC = 9c
|
||||
LD = 9l
|
||||
PKG_CONFIG ?= pkg-config
|
||||
SCANNER ?= wayland-scanner
|
||||
CFLAGS ?= -O2 -g
|
||||
WARN_CFLAGS = -Wall -Wextra
|
||||
IBUS_CFLAGS = $(shell $(PKG_CONFIG) --cflags dbus-1 xkbcommon)
|
||||
@@ -11,34 +12,50 @@ TEXT_CFLAGS = $(shell $(PKG_CONFIG) --cflags pangocairo cairo fontconfig)
|
||||
TEXT_LIBS = $(shell $(PKG_CONFIG) --libs pangocairo cairo fontconfig)
|
||||
POPUP_CFLAGS = $(shell $(PKG_CONFIG) --cflags xcb-aux xcb-randr)
|
||||
POPUP_LIBS = $(shell $(PKG_CONFIG) --libs xcb-aux xcb-randr)
|
||||
WL_CFLAGS = $(shell $(PKG_CONFIG) --cflags wayland-client xkbcommon)
|
||||
WL_LIBS = $(shell $(PKG_CONFIG) --libs wayland-client xkbcommon)
|
||||
PROJECT_CPPFLAGS =
|
||||
PROJECT_LDLIBS = -lthread -lbio -lxcb $(IBUS_LIBS) $(TEXT_LIBS) \
|
||||
$(XIM_LIBS) $(POPUP_LIBS)
|
||||
$(XIM_LIBS) $(POPUP_LIBS) $(WL_LIBS)
|
||||
PROG = strans
|
||||
DOCKER_IMAGE = strans-build
|
||||
DOCKER_RUN = docker run --rm --user "$$(id -u):$$(id -g)" \
|
||||
-v "$(CURDIR):/src" $(DOCKER_IMAGE)
|
||||
|
||||
SRCS = compose.c dict.c font.c ibus.c ipc.c ko.c main.c popup_layout.c \
|
||||
srv.c str.c strans.c trie.c vi.c win.c xim.c
|
||||
srv.c str.c strans.c trie.c vi.c win.c wl.c xim.c
|
||||
OBJS = $(SRCS:.c=.o)
|
||||
# wayland-scanner writes these from the XML in proto/; only the XML is tracked.
|
||||
PROTO = imv2.c imv2.h vkv1.c vkv1.h
|
||||
PROTOOBJ = imv2.o vkv1.o
|
||||
|
||||
all: $(PROG) gtk
|
||||
|
||||
$(PROG): $(OBJS)
|
||||
$(LD) $(LDFLAGS) -o $@ $(OBJS) $(PROJECT_LDLIBS) $(LDLIBS)
|
||||
$(PROG): $(OBJS) $(PROTOOBJ)
|
||||
$(LD) $(LDFLAGS) -o $@ $(OBJS) $(PROTOOBJ) $(PROJECT_LDLIBS) $(LDLIBS)
|
||||
|
||||
$(OBJS): dat.h fn.h ipc.h
|
||||
wl.o: imv2.h vkv1.h
|
||||
compose.o ibus.o: PROJECT_CPPFLAGS += $(IBUS_CFLAGS)
|
||||
font.o: PROJECT_CPPFLAGS += $(TEXT_CFLAGS)
|
||||
win.o: PROJECT_CPPFLAGS += $(POPUP_CFLAGS)
|
||||
wl.o $(PROTOOBJ): PROJECT_CPPFLAGS += $(WL_CFLAGS)
|
||||
xim.o: PROJECT_CPPFLAGS += $(XIM_CFLAGS)
|
||||
|
||||
%.o: %.c
|
||||
$(CC) $(CPPFLAGS) $(PROJECT_CPPFLAGS) $(WARN_CFLAGS) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
imv2.h: proto/input-method-unstable-v2.xml
|
||||
$(SCANNER) client-header $< $@
|
||||
imv2.c: proto/input-method-unstable-v2.xml
|
||||
$(SCANNER) private-code $< $@
|
||||
vkv1.h: proto/virtual-keyboard-unstable-v1.xml
|
||||
$(SCANNER) client-header $< $@
|
||||
vkv1.c: proto/virtual-keyboard-unstable-v1.xml
|
||||
$(SCANNER) private-code $< $@
|
||||
|
||||
clean:
|
||||
rm -f $(OBJS) $(PROG)
|
||||
rm -f $(OBJS) $(PROG) $(PROTO) $(PROTOOBJ)
|
||||
$(MAKE) -C tests clean
|
||||
$(MAKE) -C gtk/ clean
|
||||
$(MAKE) -C bench/ clean
|
||||
|
||||
290
README.md
290
README.md
@@ -1,213 +1,176 @@
|
||||
# strans
|
||||
|
||||
strans is a small, single-user input method for Korean, Japanese, English,
|
||||
emoji, and symbols. It provides one engine for IBus (which GTK 4 and
|
||||
Qt use), XIM, and GTK 3. Vietnamese Telex is also available as a
|
||||
compatibility mode.
|
||||
emoji and symbols, with Vietnamese Telex as a compatibility mode. One
|
||||
engine serves four frontends: `zwp_input_method_v2` on Wayland — sway and
|
||||
any other compositor that offers it — IBus, which GTK 4 and Qt use, XIM,
|
||||
and a GTK 3 module.
|
||||
|
||||
## Input modes
|
||||
## Modes
|
||||
|
||||
| Key | Mode |
|
||||
| --- | --- |
|
||||
| `Ctrl+N` | Japanese Hiragana and Kanji conversion |
|
||||
| `Ctrl+K` | Japanese Katakana |
|
||||
| `Ctrl+S` | Korean Hangul (2-beolsik) |
|
||||
| `Ctrl+N` | Japanese Hiragana, converting to Kanji |
|
||||
| `Ctrl+K` | Japanese Katakana |
|
||||
| `Ctrl+T` | English |
|
||||
| `Ctrl+V` | Vietnamese Telex |
|
||||
| `Ctrl+E` | Emoji and symbol search |
|
||||
| `Ctrl+H` | One-shot Hanja search |
|
||||
| `Ctrl+H` | One-shot Hanja and symbol search |
|
||||
|
||||
Switching mode shows the mode switched to — `A`, `한`, `あ`, `ア`, `ă` — in
|
||||
the popup until the next key.
|
||||
Only a plain `Ctrl` chord is a strans key: with `Shift`, `Alt` or `Super`
|
||||
held it commits what is pending and goes to the application, so
|
||||
`Ctrl+Shift+V` still pastes. `Backspace`, `Enter`, `Tab`, `Esc` and the
|
||||
arrow and page keys do the same under `Ctrl`, so `Ctrl+Backspace` still
|
||||
deletes a word. The mode switched to — `한`, `あ`, `ア`, `ă`, `A` — shows
|
||||
in the popup until the next key. A Korean keyboard's 한/영 and 한자 keys
|
||||
stand for `Ctrl+S`/`Ctrl+T` and `Ctrl+H`; a Japanese one's 半角/全角,
|
||||
ひらがな/カタカナ, 変換 and 無変換 for the kana modes, `Space` and English.
|
||||
|
||||
A Korean keyboard's 한/영 key toggles Korean and English, and its 한자 key
|
||||
is `Ctrl+H`; a Japanese keyboard's 半角/全角 toggles Japanese and English,
|
||||
ひらがな/カタカナ switches between the two kana modes, 変換 turns Japanese
|
||||
on and then converts like `Space`, and 無変換 turns it off. Only a plain
|
||||
`Ctrl` chord is a strans key: with `Shift`, `Alt`, or `Super` held it
|
||||
commits what is pending and goes to the application, so `Ctrl+Shift+V`
|
||||
still pastes.
|
||||
## Composing
|
||||
|
||||
Hiragana mode composes a complete reading, then shows its Kanji candidates
|
||||
with none chosen: the exact readings of the dictionary, then the verbs and
|
||||
adjectives SKK keys by stem and okurigana (かく offers 確 and 書く), and
|
||||
under `Space` the reading in Katakana last, so a word the dictionary lacks
|
||||
converts to Katakana with one `Space`. `Space` or `Tab` chooses the first
|
||||
candidate and then steps on, wrapping (with `Shift`, back); `Up`/`Down` and
|
||||
`PageUp`/`PageDown` move without wrapping. Once a candidate is chosen, an unmodified `1`-`9` commits that row of the
|
||||
current page; before that the digits type. `Enter` commits the chosen candidate, or the
|
||||
reading when none is chosen, as `0` always does; so does typing on, or any
|
||||
key that ends the composition. `Backspace` deletes the last kana shown;
|
||||
romaji that made no kana stays in the reading as typed until it is mended.
|
||||
The dictionary keys a verb or adjective by its stem, so `kaku` offers the
|
||||
readings of かく and then 書く and its kin; typing the okurigana with
|
||||
`Shift`, as SKK does — `kaKu` — asks for that split first. Caps Lock is
|
||||
not `Shift` and marks nothing.
|
||||
`Backspace` and `Esc` on a chosen candidate go back to the reading, and
|
||||
`Esc` on the reading cancels it. Katakana mode does not perform Kanji
|
||||
conversion.
|
||||
Japanese composes a whole reading and then offers its Kanji with none
|
||||
chosen. `Space` and `Tab` step through the candidates and wrap (`Shift`
|
||||
reverses), the reading in Katakana last, so a word the dictionary lacks
|
||||
converts with one `Space`; `Up`/`Down` and `PageUp`/`PageDown` move without
|
||||
wrapping. 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
|
||||
Korean composes one syllable at a time. `Enter`, `Tab`, `Esc` and any key
|
||||
that is not a jamo commit the syllable and go on to the application, so
|
||||
`Esc` still leaves insert mode. Two lone consonants join into the compound
|
||||
final they make (rt → ㄳ) and a vowel typed before its consonant is
|
||||
final they make (rt → ㄳ), and a vowel typed before its consonant is
|
||||
reordered under it (kr → 가), as in libhangul.
|
||||
|
||||
Emoji and Hanja searches take over the keys until `Enter`, `Space`, or
|
||||
`1`-`9` picks a result or `Esc` cancels; they return to the previous mode
|
||||
after use, and switching language during a search commits the shown
|
||||
query. `Tab`/`Shift-Tab` wrap through the results, and a search with
|
||||
nothing typed yet shows ☺ or 漢 in the popup. The Emoji search matches the
|
||||
typed keys, and what they spell in the current language, against a prefix
|
||||
of every emoji's CLDR name and keywords in English, Korean, and Japanese,
|
||||
and against ASCII symbol aliases such as `->` and `<=`. `Ctrl+H` takes the
|
||||
syllable being composed as its query and composes on from it; `Esc` gives
|
||||
the syllable back. The Hanja dictionary converts a word as well as a
|
||||
syllable, so `Ctrl+H` and then 한자 gives 漢字, and 대한민국 gives 大韓民國;
|
||||
a syllable already committed is the application's text, not the engine's,
|
||||
and cannot be converted. Leaving the field or
|
||||
clicking elsewhere commits what is pending.
|
||||
A search takes the keys until `Enter`, `Space` or `1`-`9` picks a result or
|
||||
`Esc` cancels, then returns to the previous mode. Emoji matches the typed
|
||||
keys, and what they spell in the current mode, against a prefix of every
|
||||
emoji's CLDR name and keywords in English, Korean and Japanese, and against
|
||||
ASCII aliases such as `->` and `<=`. `Ctrl+H` takes the syllable being
|
||||
composed as its query and composes on from it, converting a word as well as
|
||||
a syllable — 한자 gives 漢字, 대한민국 gives 大韓民國 — and `Esc` gives the
|
||||
syllable back. A word is committed a syllable at a time, so the reading
|
||||
also reaches back into the text the application already holds, as far as
|
||||
the dictionary knows the whole of it: type 한자 and then `Ctrl+H` and the
|
||||
query is 한자, not 자, and picking 漢字 takes the 한 back. `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 |
|
||||
|
||||
Each XIM style is offered with StatusNothing and with StatusNone.
|
||||
|
||||
Dead keys and Compose sequences are composed by strans itself for the XIM
|
||||
and IBus frontends, from the table `XCOMPOSEFILE` or the locale names; the
|
||||
GTK 3 module leaves them to GtkIMContextSimple. Their text follows
|
||||
whatever was pending.
|
||||
|
||||
XIM text travels as `COMPOUND_TEXT`, which carries any UTF-8. A client that
|
||||
sends `XNSpotLocation` gets the popup under that spot whatever its preedit
|
||||
style, and above the line when there is no room below; otherwise the popup
|
||||
sits under the focus window, or under the client window when no focus
|
||||
window is set.
|
||||
|
||||
The popup is an X11 window sized by `GDK_SCALE` on HiDPI displays, XIM is
|
||||
X11-only, and GTK popup placement converts the GTK caret to X11 root
|
||||
coordinates. There is no native Wayland popup surface or Wayland caret
|
||||
positioning. GTK and IBus clients can still use inline preedit where their
|
||||
display environment supports it.
|
||||
On Wayland the popup is a surface the compositor places at the text cursor,
|
||||
flipping it above the line when there is no room below. Elsewhere it is an
|
||||
X11 window, placed from the XIM spot or from the GTK caret, and XIM text
|
||||
travels as `COMPOUND_TEXT`. Either way `GDK_SCALE` sizes it for HiDPI
|
||||
displays.
|
||||
|
||||
## Build and test
|
||||
|
||||
Initialize the bundled test library, then build in the Docker environment:
|
||||
|
||||
```sh
|
||||
git submodule update --init
|
||||
make docker-image
|
||||
make docker-build
|
||||
```
|
||||
|
||||
Build output:
|
||||
|
||||
```text
|
||||
strans daemon with IBus and XIM frontends
|
||||
gtk/im-strans.so GTK 3 frontend module
|
||||
```
|
||||
|
||||
Tests have three tiers:
|
||||
That leaves `strans`, the daemon, and `gtk/im-strans.so`, the GTK 3 module.
|
||||
Tests come in three tiers:
|
||||
|
||||
```sh
|
||||
make check # generated-map validation and focused unit tests
|
||||
make check-live # one IBus, GTK, XIM, and IPC daemon smoke
|
||||
make check-stress # randomized, capacity, collision, failure, and restart tests
|
||||
make check # generated-map validation and unit tests
|
||||
make check-live # one IBus, GTK, XIM and IPC daemon smoke each
|
||||
make check-stress # randomized, capacity, collision, failure and restart
|
||||
```
|
||||
|
||||
`UNITARGS` filters only the focused C unit suite, for example
|
||||
`make check UNITARGS=hangul`. It does not select or skip live and stress
|
||||
tests. `make docker-check` rebuilds and runs all three tiers in the
|
||||
container, and `make docker-valgrind` runs the unit suite under Valgrind
|
||||
there; run `make docker-image` first after changing `Dockerfile`.
|
||||
`make docker-check` runs all three in the container, and
|
||||
`make docker-valgrind` the unit suite under Valgrind; rerun
|
||||
`make docker-image` after changing `Dockerfile`. `UNITARGS=hangul` filters
|
||||
the unit suite alone, not the other tiers.
|
||||
|
||||
Native Linux source builds require a C toolchain, Make, and pkg-config, plus
|
||||
development files for:
|
||||
|
||||
- Plan 9 port (`9c`, `9l`, `libthread`, and `libbio`);
|
||||
- D-Bus development files;
|
||||
- XCB, XCB RandR, XCB XKB, xcb-util, xcb-imdkit, xkbcommon, and xkbcommon-x11;
|
||||
- Pango, PangoCairo, Cairo, and Fontconfig;
|
||||
- GTK 3, plus libibus and Xlib for the live clients;
|
||||
- Xvfb for the X11 live tests;
|
||||
- Python 3 and `cmp` for `make check` and the map generators;
|
||||
- a working `C.UTF-8` locale and fonts covering Latin, CJK, and emoji.
|
||||
|
||||
The package names used by the canonical Arch Linux image are listed in
|
||||
[`Dockerfile`](Dockerfile).
|
||||
A native build wants a C toolchain, Make, pkg-config and Plan 9 port, plus
|
||||
development files for D-Bus, XCB and xcb-imdkit, Wayland and
|
||||
`wayland-scanner`, xkbcommon, Pango and Cairo, and GTK 3; the tests also
|
||||
want Xvfb, Python 3 and fonts covering Latin, CJK and emoji.
|
||||
[`Dockerfile`](Dockerfile) names the exact packages.
|
||||
|
||||
## Run
|
||||
|
||||
The popup uses the normal system fonts found by Pango and Fontconfig. Run
|
||||
it directly from the build tree:
|
||||
|
||||
```sh
|
||||
./run.sh
|
||||
# equivalently
|
||||
./strans map
|
||||
./run.sh # restart the daemon in the background
|
||||
./strans map # or run it in the foreground
|
||||
```
|
||||
|
||||
`run.sh` stops every same-user process whose exact name is `strans`, starts one
|
||||
build-tree daemon in the background, and returns to the shell. It requires
|
||||
`pkill` from procps. Startup and frontend failures remain visible on the
|
||||
caller's standard error. A service supervisor should instead run
|
||||
`./strans map` directly in the foreground, with the session's
|
||||
`XDG_RUNTIME_DIR` and `DISPLAY`: the GTK module finds the daemon at
|
||||
`$XDG_RUNTIME_DIR/strans.sock` (else `/tmp/strans.UID`), and IBus clients
|
||||
through the address file libibus expects under `~/.config/ibus/bus/`,
|
||||
which strans writes as `ibus-daemon` would, named after `WAYLAND_DISPLAY`
|
||||
when there is one and `DISPLAY` otherwise.
|
||||
`./strans DIR` reads `hira.map`, `kata.map`, `telex.map`, `kanji.dict`,
|
||||
`emoji.dict` and `hanja.dict` from `DIR` at runtime; nothing but the GTK
|
||||
module is installed. A service supervisor wants the session's
|
||||
`XDG_RUNTIME_DIR` and its `WAYLAND_DISPLAY` or `DISPLAY`: the GTK module
|
||||
finds the daemon at `$XDG_RUNTIME_DIR/strans.sock`, else
|
||||
`/tmp/strans.UID`, and IBus clients through the address file libibus
|
||||
expects under `~/.config/ibus/bus/`, which strans writes as `ibus-daemon`
|
||||
would.
|
||||
|
||||
The build does not copy or install the daemon or its data. `./strans DIR`
|
||||
opens `hira.map`, `kata.map`, `telex.map`, `kanji.dict`, `emoji.dict`, and
|
||||
`hanja.dict` from `DIR` at runtime; `run.sh` passes the tracked `map/`
|
||||
directory. Only the GTK subdirectory has install and uninstall targets.
|
||||
strans shows one popup per session, so it picks a frontend at startup: a
|
||||
compositor offering `zwp_input_method_v2` gets the Wayland frontend, and
|
||||
then neither XIM nor the X11 popup runs; every other session gets XIM. The
|
||||
IBus endpoint and the GTK 3 module are served in both.
|
||||
|
||||
Configure clients as needed:
|
||||
On Wayland, then, set none of the variables below. GTK, Qt and Firefox
|
||||
speak text-input-v3 themselves — GTK 4 binds it with `GTK_IM_MODULE` unset
|
||||
— and setting these is what pushes them off the path that works: such a
|
||||
client still composes inline but shows no candidates, because the popup
|
||||
belongs to the Wayland frontend. Chromium is beyond help either way, since
|
||||
it asks for `text-input-v1`, which wlroots does not implement.
|
||||
|
||||
```sh
|
||||
# XIM
|
||||
export XMODIFIERS=@im=strans
|
||||
|
||||
# GTK 3 module
|
||||
doas make -C gtk install
|
||||
export XMODIFIERS=@im=strans # XIM
|
||||
doas make -C gtk install # GTK 3 module
|
||||
export GTK_IM_MODULE=strans
|
||||
|
||||
# IBus client example
|
||||
GLFW_IM_MODULE=ibus kitty
|
||||
GLFW_IM_MODULE=ibus kitty # IBus client example
|
||||
```
|
||||
|
||||
Building `gtk/im-strans.so` requires GTK development files; installing one
|
||||
built elsewhere, in the container for instance, does not. The install
|
||||
target uses `GTK_MODULE_DIR` when it is set, else GTK's pkg-config
|
||||
variables, else the directory the installed `gtk-query-immodules-3.0`
|
||||
reports. Packagers and cross builds should set `GTK_MODULE_DIR`, and a
|
||||
prebuilt module must still match the target architecture and library ABI.
|
||||
|
||||
`DESTDIR` staging is supported and deliberately skips the host module-cache
|
||||
update. A native install needs the GTK runtime query utility so it can refresh
|
||||
that cache. Remove a native installation with:
|
||||
|
||||
```sh
|
||||
doas make -C gtk uninstall
|
||||
```
|
||||
|
||||
strans provides its own IBus endpoint; `ibus-daemon` and fcitx are not
|
||||
required. IBus clients that process keys synchronously — GTK 4, or GTK 3
|
||||
with `IBUS_ENABLE_SYNC_MODE=1` — get their commits and preedits through
|
||||
`PostProcessKeyEvent`, in order with the key. Without `DISPLAY`, the daemon and IBus frontend still work, but XIM
|
||||
is not started and the popup is disabled.
|
||||
strans provides its own IBus endpoint, so `ibus-daemon` and fcitx are not
|
||||
needed. Building the GTK module wants GTK development files; installing
|
||||
one built elsewhere does not. The install target takes `GTK_MODULE_DIR`
|
||||
when set, else asks GTK where its modules live; `doas make -C gtk uninstall`
|
||||
removes it. With neither `WAYLAND_DISPLAY` nor `DISPLAY` the daemon still
|
||||
serves IBus and its socket, but nothing draws a popup.
|
||||
|
||||
## Dictionary data
|
||||
|
||||
After changing an input source, regenerate and verify the maps and
|
||||
dictionaries:
|
||||
After changing an input source, regenerate and verify:
|
||||
|
||||
```sh
|
||||
python3 map/mktelex.py >map/telex.map
|
||||
@@ -216,17 +179,16 @@ map/mkhanja >map/hanja.dict
|
||||
make verify-map
|
||||
```
|
||||
|
||||
See [`map/README`](map/README) for the emoji, Hanja, and Japanese data and
|
||||
where it comes from.
|
||||
[`map/README`](map/README) says where the emoji, Hanja and Japanese data
|
||||
comes from.
|
||||
|
||||
## Benchmark
|
||||
|
||||
`make bench` builds `bench/bench`. With the daemon stopped, `./bench.sh` starts
|
||||
one build-tree instance, warms it up, and records the workload with Linux
|
||||
`perf`. It requires `perf` and permission to profile the daemon, and writes
|
||||
`make bench` builds `bench/bench`. With the daemon stopped, `./bench.sh`
|
||||
starts one, warms it up and records the workload with Linux `perf` into
|
||||
`bench/perf.data`.
|
||||
|
||||
## Licensing
|
||||
|
||||
Third-party license notices are in [`LICENSES`](LICENSES). The repository does
|
||||
not currently declare a license for the strans source as a whole.
|
||||
Third-party notices are in [`LICENSES`](LICENSES). The repository declares
|
||||
no license for the strans source as a whole.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
44
compose.c
44
compose.c
@@ -5,7 +5,13 @@
|
||||
#include <xkbcommon/xkbcommon.h>
|
||||
#include <xkbcommon/xkbcommon-compose.h>
|
||||
|
||||
static struct xkb_compose_state *cstate;
|
||||
/*
|
||||
* One state per frontend, all made before the procs are: xkbcommon locks
|
||||
* nothing, and even its table refcount is a plain increment. cowner is
|
||||
* the context whose sequence a state holds.
|
||||
*/
|
||||
static struct xkb_compose_state *cstate[Ncompose];
|
||||
static void *cowner[Ncompose];
|
||||
|
||||
/* Dead keys and Compose: no frontend's client does them for us. */
|
||||
void
|
||||
@@ -14,6 +20,7 @@ composeinit(void)
|
||||
struct xkb_context *ctx;
|
||||
struct xkb_compose_table *table;
|
||||
char *locale;
|
||||
int i;
|
||||
|
||||
locale = setlocale(LC_CTYPE, "");
|
||||
ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
|
||||
@@ -22,30 +29,51 @@ composeinit(void)
|
||||
table = xkb_compose_table_new_from_locale(ctx,
|
||||
locale != nil ? locale : "C", XKB_COMPOSE_COMPILE_NO_FLAGS);
|
||||
if(table != nil)
|
||||
cstate = xkb_compose_state_new(table, XKB_COMPOSE_STATE_NO_FLAGS);
|
||||
for(i = 0; i < Ncompose; i++)
|
||||
cstate[i] = xkb_compose_state_new(table,
|
||||
XKB_COMPOSE_STATE_NO_FLAGS);
|
||||
xkb_compose_table_unref(table);
|
||||
xkb_context_unref(ctx);
|
||||
}
|
||||
|
||||
/* The context is going, and the sequence it left half typed with it. */
|
||||
void
|
||||
composedrop(int who, void *owner)
|
||||
{
|
||||
if(cstate[who] == nil || owner != cowner[who])
|
||||
return;
|
||||
xkb_compose_state_reset(cstate[who]);
|
||||
cowner[who] = nil;
|
||||
}
|
||||
|
||||
/*
|
||||
* Feeds sym to the Compose table. Returns 1 while a sequence is
|
||||
* unfinished: the key belongs to the sequence, not to the engine. A
|
||||
* finished sequence leaves its text in buf, which the caller commits
|
||||
* after whatever the engine had pending.
|
||||
* after whatever the engine had pending. A sequence belongs to the
|
||||
* context that started it: a key from another one starts over.
|
||||
*/
|
||||
int
|
||||
composekey(u32int sym, char *buf, int n)
|
||||
composekey(int who, void *owner, u32int sym, char *buf, int n)
|
||||
{
|
||||
struct xkb_compose_state *cs;
|
||||
|
||||
buf[0] = '\0';
|
||||
if(cstate == nil ||
|
||||
xkb_compose_state_feed(cstate, sym) != XKB_COMPOSE_FEED_ACCEPTED)
|
||||
cs = cstate[who];
|
||||
if(cs == nil)
|
||||
return 0;
|
||||
switch(xkb_compose_state_get_status(cstate)){
|
||||
if(owner != cowner[who]){
|
||||
xkb_compose_state_reset(cs);
|
||||
cowner[who] = owner;
|
||||
}
|
||||
if(xkb_compose_state_feed(cs, sym) != XKB_COMPOSE_FEED_ACCEPTED)
|
||||
return 0;
|
||||
switch(xkb_compose_state_get_status(cs)){
|
||||
case XKB_COMPOSE_COMPOSING:
|
||||
case XKB_COMPOSE_CANCELLED:
|
||||
return 1;
|
||||
case XKB_COMPOSE_COMPOSED:
|
||||
xkb_compose_state_get_utf8(cstate, buf, n);
|
||||
xkb_compose_state_get_utf8(cs, buf, n);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
||||
23
dat.h
23
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
|
||||
{
|
||||
@@ -29,6 +36,11 @@ enum
|
||||
Maxkouho = 128,
|
||||
Maxdisp = 9,
|
||||
|
||||
/* Content purposes whose keys the engine never sees; IBus and
|
||||
* text-input-v3 number them alike, both after GTK. */
|
||||
Purposepassword = 8,
|
||||
Purposepin = 9,
|
||||
|
||||
Colfg = 0x000000,
|
||||
Colbg = 0xffffff,
|
||||
Colsep = 0xd0d0d0,
|
||||
@@ -36,6 +48,15 @@ enum
|
||||
Colselfg = 0xffffff,
|
||||
};
|
||||
|
||||
/* Each frontend composes on its own: they run in different procs. */
|
||||
enum
|
||||
{
|
||||
Composeibus,
|
||||
Composexim,
|
||||
Composewl,
|
||||
Ncompose,
|
||||
};
|
||||
|
||||
/* Popup metrics in pixels; popupscale is GDK_SCALE, for HiDPI. */
|
||||
extern int popupscale;
|
||||
#define Fontsz (32*popupscale)
|
||||
@@ -186,6 +207,7 @@ enum
|
||||
struct Keyres
|
||||
{
|
||||
int eaten; /* Keycap: the caller is the owner and clientpre took. */
|
||||
int del; /* runes of the client's own text to take back first */
|
||||
Str commit;
|
||||
Str preedit;
|
||||
};
|
||||
@@ -198,6 +220,7 @@ struct Keyreq
|
||||
u32int ks;
|
||||
u32int mod;
|
||||
Caret caret;
|
||||
Str surround; /* the client's own text just before the cursor */
|
||||
Channel *reply;
|
||||
};
|
||||
|
||||
|
||||
6
fn.h
6
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*);
|
||||
@@ -43,10 +44,13 @@ void srvinit(void);
|
||||
void srvthread(void*);
|
||||
void ibusthread(void*);
|
||||
void ximthread(void*);
|
||||
int wlinit(void);
|
||||
void wlthread(void*);
|
||||
int keymeaningful(u32int);
|
||||
|
||||
void composeinit(void);
|
||||
int composekey(u32int, char*, int);
|
||||
void composedrop(int, void*);
|
||||
int composekey(int, void*, u32int, char*, int);
|
||||
|
||||
void* emalloc(ulong);
|
||||
void* erealloc(void*, ulong);
|
||||
|
||||
77
gtk/main.c
77
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)
|
||||
{
|
||||
@@ -264,7 +332,7 @@ kpress(GtkIMContext *ctx, GdkEventKey *ev)
|
||||
return simplefilter(ctx, ev, 0);
|
||||
key = ipckeysym(ev->keyval, gdk_keyval_to_unicode(ev->keyval));
|
||||
mod = ipcmod(ev->state);
|
||||
if(im->private || key == 0){
|
||||
if(im->private || ishidden(im) || key == 0){
|
||||
r = simplefilter(ctx, ev, 0);
|
||||
/* A dead key starts a compose: pending text was typed first. */
|
||||
if(im->simpleactive)
|
||||
@@ -274,7 +342,7 @@ kpress(GtkIMContext *ctx, GdkEventKey *ev)
|
||||
if(srvconnect(im) < 0)
|
||||
return simplefilter(ctx, ev, 0);
|
||||
/* A retained GDK window may have moved since the last cursor report. */
|
||||
if(sendcaret(im) < 0){
|
||||
if(sendcaret(im) < 0 || sendsurround(im, ctx) < 0){
|
||||
srvclose(im);
|
||||
return simplefilter(ctx, ev, 0);
|
||||
}
|
||||
@@ -286,6 +354,11 @@ kpress(GtkIMContext *ctx, GdkEventKey *ev)
|
||||
}
|
||||
if(im->usepreedit && commit[0] != '\0' && im->prelen > 0)
|
||||
setpreedit(im, "", 0);
|
||||
/* The reading reached into the widget's own text: give it back. */
|
||||
if(resp.del > 0){
|
||||
gtk_im_context_delete_surrounding(ctx, -resp.del, resp.del);
|
||||
im->surroundsent = 0;
|
||||
}
|
||||
if(commit[0] != '\0')
|
||||
g_signal_emit_by_name(ctx, "commit", commit);
|
||||
if(im->usepreedit)
|
||||
|
||||
112
ibus.c
112
ibus.c
@@ -16,8 +16,7 @@ enum
|
||||
Relmask = 1<<30,
|
||||
/* IBus wire constants; the daemon does not link libibus. */
|
||||
Ibuscappreedit = 1<<0,
|
||||
Ibuspurposepassword = 8,
|
||||
Ibuspurposepin = 9,
|
||||
Ibuscapsurround = 1<<5,
|
||||
Ibusattrunderline = 1,
|
||||
Ibusunderlinesingle = 1,
|
||||
Ibuspreeditclear = 0,
|
||||
@@ -53,6 +52,7 @@ struct Ictx
|
||||
int npost;
|
||||
Post post[Maxpost];
|
||||
Caret caret;
|
||||
Str surround; /* the client's own text just before the cursor */
|
||||
};
|
||||
|
||||
static DBusWatch *watches[Maxwatches];
|
||||
@@ -85,9 +85,10 @@ unlinkaddr(void)
|
||||
}
|
||||
|
||||
static int
|
||||
addrnote(void*, char*)
|
||||
addrnote(void*, char *note)
|
||||
{
|
||||
unlinkaddr();
|
||||
if(notefatal(note))
|
||||
unlinkaddr();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -305,6 +306,11 @@ sendrequest(Ictx *ctx, int op, u32int ks, u32int mod, Keyres *res)
|
||||
kr.ks = ks;
|
||||
kr.mod = mod;
|
||||
kr.caret = ctx->caret;
|
||||
/* A client that reads the key's effects back afterwards takes a
|
||||
* DeleteSurroundingText signal after them, which is too late to be
|
||||
* a taking back: it is never asked for text it cannot lose. */
|
||||
if(!ctx->postprocess)
|
||||
kr.surround = ctx->surround;
|
||||
kr.reply = replyc;
|
||||
chansend(keyc, &kr);
|
||||
chanrecv(replyc, res);
|
||||
@@ -313,8 +319,8 @@ sendrequest(Ictx *ctx, int op, u32int ks, u32int mod, Keyres *res)
|
||||
static int
|
||||
hidden(Ictx *ctx)
|
||||
{
|
||||
return ctx->purpose == Ibuspurposepassword ||
|
||||
ctx->purpose == Ibuspurposepin;
|
||||
return ctx->purpose == Purposepassword ||
|
||||
ctx->purpose == Purposepin;
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -465,6 +471,34 @@ emitpreedit(Ictx *ctx, const char *text)
|
||||
preowner = nil;
|
||||
}
|
||||
|
||||
/* The client is to take back the runes the reading reached into. */
|
||||
static void
|
||||
emitdelete(Ictx *ctx, int n)
|
||||
{
|
||||
DBusMessage *sig;
|
||||
dbus_int32_t off;
|
||||
dbus_uint32_t nrune;
|
||||
|
||||
off = -n;
|
||||
nrune = n;
|
||||
sig = newsignal(ctx->path, "DeleteSurroundingText");
|
||||
dbus_message_append_args(sig, DBUS_TYPE_INT32, &off,
|
||||
DBUS_TYPE_UINT32, &nrune, DBUS_TYPE_INVALID);
|
||||
dbus_connection_send(ctx->conn, sig, nil);
|
||||
dbus_message_unref(sig);
|
||||
}
|
||||
|
||||
/* Nothing arrives until the client is asked to send it. */
|
||||
static void
|
||||
emitrequiresurround(Ictx *ctx)
|
||||
{
|
||||
DBusMessage *sig;
|
||||
|
||||
sig = newsignal(ctx->path, "RequireSurroundingText");
|
||||
dbus_connection_send(ctx->conn, sig, nil);
|
||||
dbus_message_unref(sig);
|
||||
}
|
||||
|
||||
/* Another frontend may have taken the engine; then our preedit is stale. */
|
||||
static void
|
||||
checkpreowner(void)
|
||||
@@ -511,6 +545,7 @@ static void
|
||||
dropcontext(Ictx *ctx)
|
||||
{
|
||||
releasecontext(ctx);
|
||||
composedrop(Composeibus, ctx);
|
||||
if(preowner == ctx)
|
||||
preowner = nil;
|
||||
memset(ctx, 0, sizeof *ctx);
|
||||
@@ -538,7 +573,7 @@ processkey(Ictx *ctx, u32int sym, u32int state, char *text, int n, Keyres *res)
|
||||
return 0;
|
||||
ctx->focused = 1;
|
||||
/* An IBus client drops a dead key we do not take: compose it here. */
|
||||
if(composekey(sym, text, n))
|
||||
if(composekey(Composeibus, ctx, sym, text, n))
|
||||
return -1;
|
||||
if(text[0] != '\0')
|
||||
/* Composed text follows whatever was pending. */
|
||||
@@ -665,6 +700,8 @@ handlekey(DBusConnection *c, DBusMessage *m, Ictx *ctx)
|
||||
ctx->keying = 1;
|
||||
if(restart)
|
||||
emitpreedit(ctx, "");
|
||||
if(res.del > 0)
|
||||
emitdelete(ctx, res.del);
|
||||
if(commit[0] != '\0')
|
||||
emitcommit(ctx, commit);
|
||||
if(text[0] != '\0')
|
||||
@@ -732,6 +769,7 @@ handlefocusout(DBusConnection *c, DBusMessage *m, Ictx *ctx)
|
||||
flushcontext(ctx, Keyrelease);
|
||||
ctx->focused = 0;
|
||||
memset(&ctx->caret, 0, sizeof ctx->caret);
|
||||
sclear(&ctx->surround);
|
||||
return reply(c, m, DBUS_TYPE_INVALID, nil);
|
||||
}
|
||||
|
||||
@@ -746,6 +784,8 @@ handlecap(DBusConnection *c, DBusMessage *m, Ictx *ctx)
|
||||
dbus_message_get_args(m, nil, DBUS_TYPE_UINT32, &cap, DBUS_TYPE_INVALID);
|
||||
old = ctx->cap;
|
||||
ctx->cap = cap;
|
||||
if((cap & ~old) & Ibuscapsurround)
|
||||
emitrequiresurround(ctx);
|
||||
if(ctx->focused){
|
||||
sendrequest(ctx, Keycap, 0, 0, &res);
|
||||
if((old & Ibuscappreedit) != (ctx->cap & Ibuscappreedit)){
|
||||
@@ -759,6 +799,57 @@ handlecap(DBusConnection *c, DBusMessage *m, Ictx *ctx)
|
||||
return reply(c, m, DBUS_TYPE_INVALID, nil);
|
||||
}
|
||||
|
||||
/* IBusText is (sa{sv}sv); the string in it is all we want. */
|
||||
static int
|
||||
getibustext(DBusMessageIter *value, const char **s)
|
||||
{
|
||||
DBusMessageIter st;
|
||||
int i;
|
||||
|
||||
if(dbus_message_iter_get_arg_type(value) != DBUS_TYPE_STRUCT)
|
||||
return 0;
|
||||
dbus_message_iter_recurse(value, &st);
|
||||
for(i = 0; i < 2; i++)
|
||||
if(!dbus_message_iter_next(&st))
|
||||
return 0;
|
||||
if(dbus_message_iter_get_arg_type(&st) != DBUS_TYPE_STRING)
|
||||
return 0;
|
||||
dbus_message_iter_get_basic(&st, s);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* The bytes the first n runes of s take; IBus counts a cursor in runes. */
|
||||
static int
|
||||
runebytes(const char *s, int n)
|
||||
{
|
||||
Rune r;
|
||||
char *p;
|
||||
|
||||
for(p = (char*)s; n > 0 && *p != '\0'; n--)
|
||||
p += chartorune(&r, p);
|
||||
return p - s;
|
||||
}
|
||||
|
||||
/* The text around the cursor; a reading can use what comes before it. */
|
||||
static DBusHandlerResult
|
||||
handlesurround(DBusConnection *c, DBusMessage *m, Ictx *ctx)
|
||||
{
|
||||
DBusMessageIter it, v;
|
||||
dbus_uint32_t cursor;
|
||||
const char *s;
|
||||
|
||||
if(!dbus_message_iter_init(m, &it))
|
||||
return handleerror(c, m, DBUS_ERROR_INVALID_ARGS,
|
||||
"SetSurroundingText expects text, cursor, and anchor");
|
||||
dbus_message_iter_recurse(&it, &v);
|
||||
if(!getibustext(&v, &s) || !dbus_message_iter_next(&it))
|
||||
return handleerror(c, m, DBUS_ERROR_INVALID_ARGS,
|
||||
"SetSurroundingText expects an IBusText");
|
||||
dbus_message_iter_get_basic(&it, &cursor);
|
||||
stail(&ctx->surround, (char*)s, runebytes(s, cursor));
|
||||
return reply(c, m, DBUS_TYPE_INVALID, nil);
|
||||
}
|
||||
|
||||
static int
|
||||
getproperty(DBusMessage *m, const char **iface, const char **name,
|
||||
DBusMessageIter *value)
|
||||
@@ -889,7 +980,10 @@ handlecursor(DBusConnection *c, DBusMessage *m, Ictx *ctx)
|
||||
return reply(c, m, DBUS_TYPE_INVALID, nil);
|
||||
}
|
||||
|
||||
/* SetEngine: there is one engine, so any name is fine. */
|
||||
/*
|
||||
* Accepted and thrown away: there is one engine, so any name is fine,
|
||||
* and a relative cursor location is surface local, not a caret.
|
||||
*/
|
||||
static DBusHandlerResult
|
||||
handleok(DBusConnection *c, DBusMessage *m, Ictx*)
|
||||
{
|
||||
@@ -908,7 +1002,9 @@ static struct {
|
||||
{"Reset", "", handlereset},
|
||||
{"Destroy", "", handledestroy},
|
||||
{"SetCursorLocation", "iiii", handlecursor},
|
||||
{"SetCursorLocationRelative", "iiii", handleok},
|
||||
{"SetCapabilities", "u", handlecap},
|
||||
{"SetSurroundingText", "vuu", handlesurround},
|
||||
{"SetEngine", "s", handleok},
|
||||
};
|
||||
|
||||
|
||||
38
ipc.c
38
ipc.c
@@ -252,6 +252,22 @@ ipcpackcaret(unsigned char req[Ipccaretsz], int valid, int32_t x,
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
ipcpacksurround(unsigned char req[Ipcreqsz], size_t n)
|
||||
{
|
||||
memset(req, 0, Ipcreqsz);
|
||||
req[0] = Ipcext;
|
||||
req[1] = Ipcversion;
|
||||
req[2] = Ipcopsurround;
|
||||
putlen(req + 4, n);
|
||||
}
|
||||
|
||||
size_t
|
||||
ipcsurroundlen(const unsigned char req[Ipcreqsz])
|
||||
{
|
||||
return getlen(req + 4);
|
||||
}
|
||||
|
||||
void
|
||||
ipcunpackreq(const unsigned char req[Ipcreqsz], int *want, uint32_t *mod,
|
||||
uint32_t *key)
|
||||
@@ -281,6 +297,11 @@ ipcreqtype(const unsigned char req[Ipcreqsz])
|
||||
if(req[0] != Ipcext || req[3] > 1)
|
||||
return Ipcunknown;
|
||||
return Ipccaret;
|
||||
case Ipcopsurround:
|
||||
if(req[0] != Ipcext || req[3] != 0 ||
|
||||
getlen(req + 4) > Ipcfieldmax)
|
||||
return Ipcunknown;
|
||||
return Ipcsurround;
|
||||
}
|
||||
return Ipcunknown;
|
||||
}
|
||||
@@ -305,19 +326,21 @@ ipcreqreset(const unsigned char req[Ipcreqsz])
|
||||
}
|
||||
|
||||
int
|
||||
ipcpackresp(unsigned char *dst, size_t cap, int eaten,
|
||||
ipcpackresp(unsigned char *dst, size_t cap, int eaten, int del,
|
||||
const char *commit, size_t ncommit, const char *preedit, size_t npreedit,
|
||||
int want)
|
||||
{
|
||||
size_t n;
|
||||
|
||||
if(ncommit > Ipcfieldmax || npreedit > Ipcfieldmax)
|
||||
if(ncommit > Ipcfieldmax || npreedit > Ipcfieldmax ||
|
||||
del < 0 || del > 0xff)
|
||||
return -1;
|
||||
n = Ipcresphdrsz + ncommit + (want ? Ipclensz + npreedit : 0);
|
||||
if(cap < n)
|
||||
return -1;
|
||||
dst[0] = eaten != 0;
|
||||
putlen(dst + 1, ncommit);
|
||||
dst[1] = del;
|
||||
putlen(dst + 2, ncommit);
|
||||
memcpy(dst + Ipcresphdrsz, commit, ncommit);
|
||||
if(want){
|
||||
putlen(dst + Ipcresphdrsz + ncommit, npreedit);
|
||||
@@ -398,7 +421,7 @@ readfield(int fd, char *dst, int64_t until)
|
||||
int
|
||||
ipcreadresp(int fd, int want, char *commit, char *preedit, Ipcresp *resp)
|
||||
{
|
||||
unsigned char eaten;
|
||||
unsigned char head[2];
|
||||
int64_t until;
|
||||
int n;
|
||||
|
||||
@@ -406,13 +429,14 @@ ipcreadresp(int fd, int want, char *commit, char *preedit, Ipcresp *resp)
|
||||
preedit[0] = '\0';
|
||||
memset(resp, 0, sizeof *resp);
|
||||
until = deadline();
|
||||
if(readwait(fd, &eaten, 1, until) < 0)
|
||||
if(readwait(fd, head, sizeof head, until) < 0)
|
||||
return -1;
|
||||
if(eaten > 1){
|
||||
if(head[0] > 1){
|
||||
errno = EPROTO;
|
||||
return -1;
|
||||
}
|
||||
resp->eaten = eaten;
|
||||
resp->eaten = head[0];
|
||||
resp->del = head[1];
|
||||
n = readfield(fd, commit, until);
|
||||
if(n < 0)
|
||||
return -1;
|
||||
|
||||
21
ipc.h
21
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,
|
||||
@@ -57,7 +64,7 @@ enum
|
||||
Ipcreqsz = 6,
|
||||
Ipccaretsz = 16,
|
||||
Ipclensz = 2,
|
||||
Ipcresphdrsz = 1 + Ipclensz,
|
||||
Ipcresphdrsz = 2 + Ipclensz,
|
||||
Ipcfieldmax = 256,
|
||||
Ipcmaxresp = Ipcresphdrsz + Ipcfieldmax + Ipclensz + Ipcfieldmax,
|
||||
Ipcwaitms = 250,
|
||||
@@ -69,12 +76,14 @@ enum
|
||||
Ipckey,
|
||||
Ipccap,
|
||||
Ipccaret,
|
||||
Ipcsurround,
|
||||
};
|
||||
|
||||
typedef struct Ipcresp Ipcresp;
|
||||
struct Ipcresp
|
||||
{
|
||||
int eaten;
|
||||
int del; /* runes of the client's own text to take back first */
|
||||
/* Bytes copied to each caller buffer, excluding its trailing NUL. */
|
||||
size_t commitlen;
|
||||
size_t preeditlen;
|
||||
@@ -86,11 +95,13 @@ void ipcpackreq(unsigned char[Ipcreqsz], int, uint32_t, uint32_t);
|
||||
void ipcpackreset(unsigned char[Ipcreqsz], int);
|
||||
void ipcpackcap(unsigned char[Ipcreqsz], int);
|
||||
void ipcpackcaret(unsigned char[Ipccaretsz], int, int32_t, int32_t, int32_t);
|
||||
void ipcpacksurround(unsigned char[Ipcreqsz], size_t);
|
||||
size_t ipcsurroundlen(const unsigned char[Ipcreqsz]);
|
||||
void ipcunpackreq(const unsigned char[Ipcreqsz], int*, uint32_t*, uint32_t*);
|
||||
int ipcunpackcaret(const unsigned char[Ipccaretsz], int*, int32_t*, int32_t*, int32_t*);
|
||||
int ipcreqtype(const unsigned char[Ipcreqsz]);
|
||||
int ipcreqreset(const unsigned char[Ipcreqsz]);
|
||||
int ipcpackresp(unsigned char*, size_t, int, const char*, size_t, const char*, size_t, int);
|
||||
int ipcpackresp(unsigned char*, size_t, int, int, const char*, size_t, const char*, size_t, int);
|
||||
int ipcreadn(int, void*, size_t);
|
||||
int ipcsend(int, const void*, size_t);
|
||||
int ipcreadresp(int, int, char*, char*, Ipcresp*);
|
||||
|
||||
20
main.c
20
main.c
@@ -47,21 +47,31 @@ erealloc(void *p, ulong n)
|
||||
void
|
||||
threadmain(int argc, char **argv)
|
||||
{
|
||||
char *display;
|
||||
char *display, *scale;
|
||||
|
||||
if(argc != 2)
|
||||
usage();
|
||||
|
||||
/* Whichever popup runs is sized from it, and setfont reads Fontsz. */
|
||||
scale = getenv("GDK_SCALE");
|
||||
if(scale != nil && atoi(scale) > 1)
|
||||
popupscale = min(atoi(scale), 4);
|
||||
drawc = chancreate(sizeof(Drawcmd), 4);
|
||||
keyc = chancreate(sizeof(Keyreq), 0);
|
||||
langinit(argv[1]);
|
||||
composeinit();
|
||||
srvinit();
|
||||
proccreate(drawthread, nil, 16384);
|
||||
proccreate(srvthread, nil, 16384);
|
||||
proccreate(ibusthread, nil, 32768);
|
||||
display = getenv("DISPLAY");
|
||||
if(display != nil && display[0] != '\0')
|
||||
proccreate(ximthread, nil, 32768);
|
||||
/* One popup per session: the Wayland frontend draws its own, so the
|
||||
* X11 one and the XIM it serves stand down. */
|
||||
if(getenv("WAYLAND_DISPLAY") != nil && wlinit())
|
||||
proccreate(wlthread, nil, 32768);
|
||||
else{
|
||||
proccreate(drawthread, nil, 16384);
|
||||
display = getenv("DISPLAY");
|
||||
if(display != nil && display[0] != '\0')
|
||||
proccreate(ximthread, nil, 32768);
|
||||
}
|
||||
imthread(nil);
|
||||
}
|
||||
|
||||
49
map/README
49
map/README
@@ -1,42 +1,53 @@
|
||||
# Dictionary data
|
||||
|
||||
## Korean Hanja data
|
||||
## Korean Hanja and symbol data
|
||||
|
||||
`hanja.src` is the tracked, reviewable source for Korean Hanja conversion.
|
||||
Every data row is Hanja, a tab, and its modern Hangul reading, a character
|
||||
or a word:
|
||||
`hanja.src` and `mssymbol.src` are the tracked, reviewable sources for what
|
||||
the Hanja search converts. Every data row is a result, a tab, and its
|
||||
Hangul reading. A Hanja reads as a syllable or a word; a symbol reads as
|
||||
the lone consonant a Korean keyboard's 한자 key offers it under:
|
||||
|
||||
```
|
||||
漢 한
|
||||
漢字 한자
|
||||
※ ㅁ
|
||||
```
|
||||
|
||||
`mkhanja` validates that contract and groups rows by their reading to
|
||||
produce the runtime dictionary. Candidate order follows source order. The
|
||||
source keeps all retained pairs for review; the generated dictionary stores
|
||||
the first 128 candidates per reading because that is the engine's lookup
|
||||
limit.
|
||||
`mkhanja` validates that contract and groups the rows of both sources by
|
||||
their reading to produce the runtime dictionary. The two keyspaces do not
|
||||
meet: a reading is either syllables or one jamo. Candidate order follows
|
||||
source order. The sources keep all retained pairs for review; the
|
||||
generated dictionary stores the first 128 candidates per reading because
|
||||
that is the engine's lookup limit.
|
||||
|
||||
The source is derived from libhangul release tag `libhangul-0.2.0`. The
|
||||
The sources are derived from libhangul release tag `libhangul-0.2.0`. The
|
||||
annotated tag object is `20afc38922e3595ee3ed5b186f2ea05afe663763`, its
|
||||
peeled commit is `41c702f5d3581325b646ef6249f1f641b0427ae0`, and
|
||||
`data/hanja/hanja.txt` has blob ID
|
||||
`199cfd70c4b306257ac2e018714a1285f7cf0ed3`. Import and regenerate it from
|
||||
the repository root with:
|
||||
`data/hanja/hanja.txt` and `data/hanja/mssymbol.txt` have blob IDs
|
||||
`199cfd70c4b306257ac2e018714a1285f7cf0ed3` and
|
||||
`31c4e63d74293b6a759d4a2005cd1e7333746631`. Import and regenerate them
|
||||
from the repository root with:
|
||||
|
||||
```sh
|
||||
curl -L https://raw.githubusercontent.com/libhangul/libhangul/41c702f5d3581325b646ef6249f1f641b0427ae0/data/hanja/hanja.txt -o upstream
|
||||
printf '%s %s\n' dd44dcc856cf542b1022d0f39c2e9b9f8805fdcc5923be80f04849ed97ce0996 upstream | sha256sum -c -
|
||||
map/libhangul2hanja upstream >map/hanja.src
|
||||
curl -L https://raw.githubusercontent.com/libhangul/libhangul/41c702f5d3581325b646ef6249f1f641b0427ae0/data/hanja/mssymbol.txt -o upstream
|
||||
printf '%s %s\n' b685a4ebe2716b25eb29c42f6da6493716ecd78b604b44a33420987d21948e99 upstream | sha256sum -c -
|
||||
map/libhangul2hanja upstream >map/mssymbol.src
|
||||
map/mkhanja >map/hanja.dict
|
||||
```
|
||||
|
||||
`libhangul2hanja` retains only rows whose reading is modern Hangul syllables
|
||||
throughout and whose value is Hanja the popup can draw: U+3400–U+4DBF,
|
||||
U+4E00–U+9FFF, or U+F900–U+FAFF. Thus jamo readings, mixed values, and
|
||||
supplementary-plane ideographs are omitted. The import preserves the upstream license header and row order.
|
||||
The retained data is BSD 3-Clause licensed by Choe Hwanjin; the complete text
|
||||
is in `LICENSES/BSD-3-Clause-libhangul-hanja.txt`.
|
||||
`libhangul2hanja` keeps a syllable reading only when its value is Hanja the
|
||||
popup can draw: U+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
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
:( ☹
|
||||
:) ☺
|
||||
<- ←
|
||||
< ← ≤ ♥ ≠
|
||||
<= ≤
|
||||
<3 ♥ 🫰 🫶
|
||||
<> ≠
|
||||
@@ -16,6 +17,7 @@
|
||||
=> ⇒
|
||||
>= ≥
|
||||
^1 ¹
|
||||
^ ¹ ² ³ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹
|
||||
^2 ²
|
||||
^3 ³
|
||||
^4 ⁴
|
||||
@@ -25,6 +27,7 @@
|
||||
^8 ⁸
|
||||
^9 ⁹
|
||||
_1 ₁
|
||||
_ ₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉
|
||||
_2 ₂
|
||||
_3 ₃
|
||||
_4 ₄
|
||||
|
||||
@@ -24,6 +24,32 @@
|
||||
;; CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
;; ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
;; POSSIBILITY OF SUCH DAMAGE.
|
||||
;; Copyright (c) 2005-2014 Choe Hwanjin
|
||||
;; All rights reserved.
|
||||
;;
|
||||
;; Redistribution and use in source and binary forms, with or without
|
||||
;; modification, are permitted provided that the following conditions are met:
|
||||
;;
|
||||
;; 1. Redistributions of source code must retain the above copyright notice,
|
||||
;; this list of conditions and the following disclaimer.
|
||||
;; 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
;; this list of conditions and the following disclaimer in the documentation
|
||||
;; and/or other materials provided with the distribution.
|
||||
;; 3. Neither the name of the author nor the names of its contributors
|
||||
;; may be used to endorse or promote products derived from this software
|
||||
;; without specific prior written permission.
|
||||
;;
|
||||
;; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
;; AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
;; IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
;; ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
;; LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
;; CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
;; SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
;; INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
;; CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
;; ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
;; POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
가 可 家 加 歌 價 街 假 佳 暇 架 价 賈 伽 柯 迦 軻 嘉 駕 嫁 稼 哥 呵 苛 袈 訶 枷 珂 茄 痂 跏 斝 葭 舸 笳 哿 坷 檟 謌 珈 砢 佉 呿 榎 仮 傢 咖 瘕 耞 宊 椵 叚 価 猳 髂 瘸 鴐 䴥 拁 㤎 榢 毠 溊 牁 豭 槚 诃 贾 轲 镓 驾 鲄 婽 尜 岢 幏 彁 徦 愘 戓 戨 斚 樖 泇 渮 滒 炣 牫 牱 犌 玍 癿 笴 糘 胢 腵 蚵 貑 跒 酠 鉫 鉲 鎵 鎶 魺 鴚 麚 㗎 㚙 㞹 㢦 㤉 㧝 㪃 㪼 㱒 㹢 䂟 䈔 䑝 䔅 䕒 䪪 䯊 䶗 嘏
|
||||
가가 假家 可呵 可嘉 家家 呵呵
|
||||
@@ -187311,3 +187337,21 @@
|
||||
힐책 詰責
|
||||
힐척 詰斥
|
||||
힐항 詰抗
|
||||
ㄱ ! ' , . / : ; ? ^ _ ` |  ̄ 、 。 · ‥ … ¨ 〃 ― ∥ \ ∼ ´ ~ ˇ ˘ ˝ ˚ ˙ ¸ ˛ ¡ ¿ ː
|
||||
ㄲ Æ Ð Ħ IJ Ŀ Ł Ø Œ Þ Ŧ Ŋ æ đ ð ħ ı ij ĸ ŀ ł ø œ ß þ ŧ ŋ ʼn
|
||||
ㄴ " ( ) [ ] { } ‘ ’ “ ” 〔 〕 〈 〉 《 》 「 」 『 』 【 】
|
||||
ㄷ + - < = > ± × ÷ ≠ ≤ ≥ ∞ ∴ ♂ ♀ ∠ ⊥ ⌒ ∂ ∇ ≡ ≒ ≪ ≫ √ ∽ ∝ ∵ ∫ ∬ ∈ ∋ ⊆ ⊇ ⊂ ⊃ ∪ ∩ ∧ ∨ ¬ ⇒ ⇔ ∀ ∃ ∮ ∑ ∏
|
||||
ㄸ ぁ あ ぃ い ぅ う ぇ え ぉ お か が き ぎ く ぐ け げ こ ご さ ざ し じ す ず せ ぜ そ ぞ た だ ち ぢ っ つ づ て で と ど な に ぬ ね の は ば ぱ ひ び ぴ ふ ぶ ぷ へ べ ぺ ほ ぼ ぽ ま み む め も ゃ や ゅ ゆ ょ よ ら り る れ ろ ゎ わ ゐ ゑ を ん
|
||||
ㄹ $ % ₩ F ′ ″ ℃ Å ¢ £ ¥ ¤ ℉ ‰ € ㎕ ㎖ ㎗ ℓ ㎘ ㏄ ㎣ ㎤ ㎥ ㎦ ㎙ ㎚ ㎛ ㎜ ㎝ ㎞ ㎟ ㎠ ㎡ ㎢ ㏊ ㎍ ㎎ ㎏ ㏏ ㎈ ㎉ ㏈ ㎧ ㎨ ㎰ ㎱ ㎲ ㎳ ㎴ ㎵ ㎶ ㎷ ㎸ ㎹ ㎀ ㎁ ㎂ ㎃ ㎄ ㎺ ㎻ ㎼ ㎽ ㎾ ㎿ ㎐ ㎑ ㎒ ㎓ ㎔ Ω ㏀ ㏁ ㎊ ㎋ ㎌ ㏖ ㏅ ㎭ ㎮ ㎯ ㏛ ㎩ ㎪ ㎫ ㎬ ㏝ ㏐ ㏓ ㏃ ㏉ ㏜ ㏆
|
||||
ㅁ # & * @ § ※ ☆ ★ ○ ● ◎ ◇ ◆ □ ■ △ ▲ ▽ ▼ → ← ↑ ↓ ↔ 〓 ◁ ◀ ▷ ▶ ♤ ♠ ♡ ♥ ♧ ♣ ⊙ ◈ ▣ ◐ ◑ ▒ ▤ ▥ ▨ ▧ ▦ ▩ ♨ ☏ ☎ ☜ ☞ ¶ † ‡ ↕ ↗ ↙ ↖ ↘ ♭ ♩ ♪ ♬ ㉿ ㈜ № ㏇ ™ ㏂ ㏘ ℡ ® ª º
|
||||
ㅂ ─ │ ┌ ┐ ┘ └ ├ ┬ ┤ ┴ ┼ ━ ┃ ┏ ┓ ┛ ┗ ┣ ┳ ┫ ┻ ╋ ┠ ┯ ┨ ┷ ┿ ┝ ┰ ┥ ┸ ╂ ┒ ┑ ┚ ┙ ┖ ┕ ┎ ┍ ┞ ┟ ┡ ┢ ┦ ┧ ┩ ┪ ┭ ┮ ┱ ┲ ┵ ┶ ┹ ┺ ┽ ┾ ╀ ╁ ╃ ╄ ╅ ╆ ╇ ╈ ╉ ╊
|
||||
ㅃ ァ ア ィ イ ゥ ウ ェ エ ォ オ カ ガ キ ギ ク グ ケ ゲ コ ゴ サ ザ シ ジ ス ズ セ ゼ ソ ゾ タ ダ チ ヂ ッ ツ ヅ テ デ ト ド ナ ニ ヌ ネ ノ ハ バ パ ヒ ビ ピ フ ブ プ ヘ ベ ペ ホ ボ ポ マ ミ ム メ モ ャ ヤ ュ ユ ョ ヨ ラ リ ル レ ロ ヮ ワ ヰ ヱ ヲ ン ヴ ヵ ヶ
|
||||
ㅅ ㉠ ㉡ ㉢ ㉣ ㉤ ㉥ ㉦ ㉧ ㉨ ㉩ ㉪ ㉫ ㉬ ㉭ ㉮ ㉯ ㉰ ㉱ ㉲ ㉳ ㉴ ㉵ ㉶ ㉷ ㉸ ㉹ ㉺ ㉻ ㈀ ㈁ ㈂ ㈃ ㈄ ㈅ ㈆ ㈇ ㈈ ㈉ ㈊ ㈋ ㈌ ㈍ ㈎ ㈏ ㈐ ㈑ ㈒ ㈓ ㈔ ㈕ ㈖ ㈗ ㈘ ㈙ ㈚ ㈛
|
||||
ㅆ А Б В Г Д Е Ё Ж З И Й К Л М Н О П Р С Т У Ф Х Ц Ч Ш Щ Ъ Ы Ь Э Ю Я а б в г д е ё ж з и й к л м н о п р с т у ф х ц ч ш щ ъ ы ь э ю я
|
||||
ㅇ ⓐ ⓑ ⓒ ⓓ ⓔ ⓕ ⓖ ⓗ ⓘ ⓙ ⓚ ⓛ ⓜ ⓝ ⓞ ⓟ ⓠ ⓡ ⓢ ⓣ ⓤ ⓥ ⓦ ⓧ ⓨ ⓩ ① ② ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨ ⑩ ⑪ ⑫ ⑬ ⑭ ⑮ ⒜ ⒝ ⒞ ⒟ ⒠ ⒡ ⒢ ⒣ ⒤ ⒥ ⒦ ⒧ ⒨ ⒩ ⒪ ⒫ ⒬ ⒭ ⒮ ⒯ ⒰ ⒱ ⒲ ⒳ ⒴ ⒵ ⑴ ⑵ ⑶ ⑷ ⑸ ⑹ ⑺ ⑻ ⑼ ⑽ ⑾ ⑿ ⒀ ⒁ ⒂
|
||||
ㅈ 0 1 2 3 4 5 6 7 8 9 ⅰ ⅱ ⅲ ⅳ ⅴ ⅵ ⅶ ⅷ ⅸ ⅹ Ⅰ Ⅱ Ⅲ Ⅳ Ⅴ Ⅵ Ⅶ Ⅷ Ⅸ Ⅹ
|
||||
ㅊ ½ ⅓ ⅔ ¼ ¾ ⅛ ⅜ ⅝ ⅞ ¹ ² ³ ⁴ ⁿ ₁ ₂ ₃ ₄
|
||||
ㅋ ㄱ ㄲ ㄳ ㄴ ㄵ ㄶ ㄷ ㄸ ㄹ ㄺ ㄻ ㄼ ㄽ ㄾ ㄿ ㅀ ㅁ ㅂ ㅃ ㅄ ㅅ ㅆ ㅇ ㅈ ㅉ ㅊ ㅋ ㅌ ㅍ ㅎ ㅏ ㅐ ㅑ ㅒ ㅓ ㅔ ㅕ ㅖ ㅗ ㅘ ㅙ ㅚ ㅛ ㅜ ㅝ ㅞ ㅟ ㅠ ㅡ ㅢ ㅣ
|
||||
ㅌ ㅥ ㅦ ㅧ ㅨ ㅩ ㅪ ㅫ ㅬ ㅭ ㅮ ㅯ ㅰ ㅱ ㅲ ㅳ ㅴ ㅵ ㅶ ㅷ ㅸ ㅹ ㅺ ㅻ ㅼ ㅽ ㅾ ㅿ ㆀ ㆁ ㆂ ㆃ ㆄ ㆅ ㆆ ㆇ ㆈ ㆉ ㆊ ㆋ ㆌ ㆍ ㆎ
|
||||
ㅍ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z
|
||||
ㅎ Α Β Γ Δ Ε Ζ Η Θ Ι Κ Λ Μ Ν Ξ Ο Π Ρ Σ Τ Υ Φ Χ Ψ Ω α β γ δ ε ζ η θ ι κ λ μ ν ξ ο π ρ σ τ υ φ χ ψ ω
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Extract Hanja readings from libhangul data."""
|
||||
"""Extract Hanja and symbol readings from libhangul data."""
|
||||
|
||||
import sys
|
||||
import unicodedata
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@@ -9,12 +10,31 @@ def ishangul(s):
|
||||
return s != "" and all(0xAC00 <= ord(c) <= 0xD7A3 for c in s)
|
||||
|
||||
|
||||
def isjamo(s):
|
||||
return len(s) == 1 and 0x3131 <= ord(s) <= 0x314E
|
||||
|
||||
|
||||
def ishanja(s):
|
||||
return s != "" and all(0x3400 <= ord(c) <= 0x4DBF
|
||||
or 0x4E00 <= ord(c) <= 0x9FFF
|
||||
or 0xF900 <= ord(c) <= 0xFAFF for c in s)
|
||||
|
||||
|
||||
def issymbol(s):
|
||||
"""One rune the popup can draw and a candidate row can carry: not a
|
||||
space, which the row separates candidates with, and not a formatting
|
||||
character, which would leave a blank candidate to pick."""
|
||||
return (len(s) == 1 and not s.isspace() and not ishanja(s)
|
||||
and not unicodedata.category(s).startswith("C"))
|
||||
|
||||
|
||||
def keeps(reading, value):
|
||||
"""A syllable reading gives Hanja; a jamo reading gives a symbol."""
|
||||
if ishangul(reading):
|
||||
return ishanja(value)
|
||||
return isjamo(reading) and issymbol(value)
|
||||
|
||||
|
||||
def extract(src, name):
|
||||
comments = []
|
||||
entries = []
|
||||
@@ -32,16 +52,16 @@ def extract(src, name):
|
||||
fields = line.split(":")
|
||||
if len(fields) != 3:
|
||||
raise ValueError(f"{name}:{lineno}: need key:value:comment")
|
||||
reading, hanja, _ = fields
|
||||
if not ishangul(reading) or not ishanja(hanja):
|
||||
reading, value, _ = fields
|
||||
if not keeps(reading, value):
|
||||
continue
|
||||
pair = (hanja, reading)
|
||||
pair = (value, reading)
|
||||
if pair in seen:
|
||||
raise ValueError(f"{name}:{lineno}: duplicate Hanja reading")
|
||||
raise ValueError(f"{name}:{lineno}: duplicate reading")
|
||||
seen.add(pair)
|
||||
entries.append(pair)
|
||||
if not entries:
|
||||
raise ValueError(f"{name}: no Hanja readings")
|
||||
raise ValueError(f"{name}: no readings")
|
||||
return comments, entries
|
||||
|
||||
|
||||
@@ -63,8 +83,8 @@ def main():
|
||||
print(line)
|
||||
if comments:
|
||||
print()
|
||||
for hanja, reading in entries:
|
||||
print(f"{hanja}\t{reading}")
|
||||
for value, reading in entries:
|
||||
print(f"{value}\t{reading}")
|
||||
except (OSError, UnicodeError, ValueError) as error:
|
||||
print(error, file=sys.stderr)
|
||||
return 1
|
||||
|
||||
63
map/mkhanja
63
map/mkhanja
@@ -1,11 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Write hanja.dict from a Hanja-per-row UTF-8 source."""
|
||||
"""Write hanja.dict from result-per-row UTF-8 sources."""
|
||||
|
||||
import sys
|
||||
import unicodedata
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
SOURCE = Path(__file__).with_name("hanja.src")
|
||||
SOURCES = [Path(__file__).with_name(name)
|
||||
for name in ("hanja.src", "mssymbol.src")]
|
||||
MAXCANDIDATES = 128
|
||||
|
||||
|
||||
@@ -13,17 +15,29 @@ def ishangul(s):
|
||||
return s != "" and all(0xAC00 <= ord(c) <= 0xD7A3 for c in s)
|
||||
|
||||
|
||||
def isjamo(s):
|
||||
return len(s) == 1 and 0x3131 <= ord(s) <= 0x314E
|
||||
|
||||
|
||||
def ishanja(s):
|
||||
return s != "" and all(0x3400 <= ord(c) <= 0x4DBF
|
||||
or 0x4E00 <= ord(c) <= 0x9FFF
|
||||
or 0xF900 <= ord(c) <= 0xFAFF for c in s)
|
||||
|
||||
|
||||
def read(path):
|
||||
def issymbol(s):
|
||||
"""One rune the popup can draw and a candidate row can carry: not a
|
||||
space, which the row separates candidates with, and not a formatting
|
||||
character, which would leave a blank candidate to pick."""
|
||||
return (len(s) == 1 and not s.isspace() and not ishanja(s)
|
||||
and not unicodedata.category(s).startswith("C"))
|
||||
|
||||
|
||||
def read(path, table, seen):
|
||||
"""Adds path's rows to table, keyed by reading, and returns its header."""
|
||||
comments = []
|
||||
table = {}
|
||||
seen = set()
|
||||
leading = True
|
||||
found = False
|
||||
with path.open(encoding="utf-8") as src:
|
||||
for lineno, raw in enumerate(src, 1):
|
||||
line = raw.rstrip("\r\n")
|
||||
@@ -36,31 +50,36 @@ def read(path):
|
||||
leading = False
|
||||
fields = line.split("\t")
|
||||
if len(fields) != 2:
|
||||
raise ValueError(f"{path}:{lineno}: need Hanja<TAB>reading")
|
||||
hanja, reading = fields
|
||||
if not ishanja(hanja):
|
||||
raise ValueError(f"{path}:{lineno}: need BMP Hanja")
|
||||
if not ishangul(reading):
|
||||
raise ValueError(f"{path}:{lineno}: need Hangul syllables")
|
||||
pair = (hanja, reading)
|
||||
raise ValueError(f"{path}:{lineno}: need result<TAB>reading")
|
||||
value, reading = fields
|
||||
if ishangul(reading):
|
||||
if not ishanja(value):
|
||||
raise ValueError(f"{path}:{lineno}: need BMP Hanja")
|
||||
elif isjamo(reading):
|
||||
if not issymbol(value):
|
||||
raise ValueError(f"{path}:{lineno}: need one symbol rune")
|
||||
else:
|
||||
raise ValueError(f"{path}:{lineno}: need a syllable or jamo "
|
||||
"reading")
|
||||
pair = (value, reading)
|
||||
if pair in seen:
|
||||
raise ValueError(f"{path}:{lineno}: duplicate Hanja reading")
|
||||
raise ValueError(f"{path}:{lineno}: duplicate reading")
|
||||
seen.add(pair)
|
||||
table.setdefault(reading, []).append(hanja)
|
||||
if not seen:
|
||||
raise ValueError(f"{path}: no Hanja readings")
|
||||
return comments, table
|
||||
found = True
|
||||
table.setdefault(reading, []).append(value)
|
||||
if not found:
|
||||
raise ValueError(f"{path}: no readings")
|
||||
return comments
|
||||
|
||||
|
||||
def main():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
if len(sys.argv) > 2:
|
||||
print(f"usage: {sys.argv[0]} [hanja.src]", file=sys.stderr)
|
||||
return 2
|
||||
path = Path(sys.argv[1]) if len(sys.argv) == 2 else SOURCE
|
||||
paths = [Path(arg) for arg in sys.argv[1:]] or SOURCES
|
||||
table = {}
|
||||
seen = set()
|
||||
try:
|
||||
comments, table = read(path)
|
||||
comments = [line for path in paths for line in read(path, table, seen)]
|
||||
for line in comments:
|
||||
print(line)
|
||||
if comments:
|
||||
|
||||
1012
map/mssymbol.src
Normal file
1012
map/mssymbol.src
Normal file
File diff suppressed because it is too large
Load Diff
@@ -10,31 +10,31 @@
|
||||
☺ :)
|
||||
|
||||
# Bare 1-9 choose from a prefix search; keep digit aliases in matching slots.
|
||||
← <-
|
||||
≤ <=
|
||||
♥ <3
|
||||
≠ <> !=
|
||||
← <- <
|
||||
≤ <= <
|
||||
♥ <3 <
|
||||
≠ <> != <
|
||||
≡ ==
|
||||
⇒ =>
|
||||
≥ >=
|
||||
¹ ^1
|
||||
² ^2
|
||||
³ ^3
|
||||
⁴ ^4
|
||||
⁵ ^5
|
||||
⁶ ^6
|
||||
⁷ ^7
|
||||
⁸ ^8
|
||||
⁹ ^9
|
||||
₁ _1
|
||||
₂ _2
|
||||
₃ _3
|
||||
₄ _4
|
||||
₅ _5
|
||||
₆ _6
|
||||
₇ _7
|
||||
₈ _8
|
||||
₉ _9
|
||||
¹ ^1 ^
|
||||
² ^2 ^
|
||||
³ ^3 ^
|
||||
⁴ ^4 ^
|
||||
⁵ ^5 ^
|
||||
⁶ ^6 ^
|
||||
⁷ ^7 ^
|
||||
⁸ ^8 ^
|
||||
⁹ ^9 ^
|
||||
₁ _1 _
|
||||
₂ _2 _
|
||||
₃ _3 _
|
||||
₄ _4 _
|
||||
₅ _5 _
|
||||
₆ _6 _
|
||||
₇ _7 _
|
||||
₈ _8 _
|
||||
₉ _9 _
|
||||
|
||||
⁽ ^(
|
||||
⁾ ^)
|
||||
|
||||
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>
|
||||
24
srv.c
24
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,6 +68,12 @@ srvreadreq(int fd, Keyreq *kr, int *want)
|
||||
kr->caret.y = y;
|
||||
kr->caret.h = h;
|
||||
return 0;
|
||||
case Ipcsurround:
|
||||
n = ipcsurroundlen(req);
|
||||
if(n > 0 && ipcreadn(fd, text, n) < 0)
|
||||
return -1;
|
||||
stail(&kr->surround, text, n);
|
||||
return 1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
@@ -78,7 +90,7 @@ clientthread(void *arg)
|
||||
Keyres res;
|
||||
uchar out[Ipcmaxresp], token;
|
||||
char commit[Maxutf], preedit[Maxutf];
|
||||
int fd, n, ncommit, npreedit, want;
|
||||
int fd, n, ncommit, npreedit, rv, want;
|
||||
|
||||
fd = (int)(uintptr)arg;
|
||||
threadsetname("client %d", fd);
|
||||
@@ -86,7 +98,9 @@ clientthread(void *arg)
|
||||
kr.reply = chancreate(sizeof(Keyres), 0);
|
||||
kr.owner = &fd;
|
||||
kr.clientpre = 1;
|
||||
while(srvreadreq(fd, &kr, &want) >= 0){
|
||||
while((rv = srvreadreq(fd, &kr, &want)) >= 0){
|
||||
if(rv != 0)
|
||||
continue;
|
||||
if(kr.op != Keycaret)
|
||||
kr.clientpre = want;
|
||||
chansend(keyc, &kr);
|
||||
@@ -97,7 +111,7 @@ clientthread(void *arg)
|
||||
npreedit = stoutf(&res.preedit, preedit, sizeof preedit);
|
||||
/* A capability reply is always eaten: it marks the extension. */
|
||||
n = ipcpackresp(out, sizeof out, kr.op == Keycap || res.eaten,
|
||||
commit, ncommit, preedit, npreedit, want);
|
||||
res.del, commit, ncommit, preedit, npreedit, want);
|
||||
if(n < 0 || ipcsend(fd, out, n) < 0)
|
||||
break;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
|
||||
138
strans.c
138
strans.c
@@ -7,17 +7,23 @@ static void *lostowner; /* the context the engine was taken from */
|
||||
static Str lost;
|
||||
static int clientpre; /* the owner draws its own preedit */
|
||||
static Caret caret;
|
||||
static Str surround; /* the owner's own text just before the cursor */
|
||||
static int takeback; /* runes of it a pick takes back before committing */
|
||||
static Rune modemark; /* the mode the last key switched to */
|
||||
static int okuriat = -1; /* where Shift marked the okurigana, or -1 */
|
||||
static Drawcmd lastdraw;
|
||||
static Emit transjp(Im*, Rune);
|
||||
static void backjp(Im*);
|
||||
|
||||
/* seed is what Ctrl+H found pending; raw the keys typed since; text both. */
|
||||
/*
|
||||
* back is the client's own text the reading reached into; seed what
|
||||
* Ctrl+H found pending; raw the keys typed since; text all three.
|
||||
*/
|
||||
typedef struct Search Search;
|
||||
struct Search
|
||||
{
|
||||
int lang;
|
||||
Str back;
|
||||
Str seed;
|
||||
Str raw;
|
||||
Str text;
|
||||
@@ -91,11 +97,12 @@ movedelta(u32int ks)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Nothing chosen yet: Down takes the first candidate, PageDown pages. */
|
||||
static void
|
||||
movekouho(int delta)
|
||||
{
|
||||
if(im.sel < 0)
|
||||
im.sel = 0;
|
||||
im.sel = delta == Maxdisp ? Maxdisp : 0;
|
||||
else
|
||||
im.sel += delta;
|
||||
if(im.sel < 0)
|
||||
@@ -184,6 +191,17 @@ impre(Im *p, Str *s)
|
||||
*s = p->pre;
|
||||
}
|
||||
|
||||
/* The query as it is still ours to show: the client has the rest. */
|
||||
static void
|
||||
searchpre(Str *s)
|
||||
{
|
||||
int i;
|
||||
|
||||
sclear(s);
|
||||
for(i = search.back.n; i < search.text.n; i++)
|
||||
sputr(s, search.text.r[i]);
|
||||
}
|
||||
|
||||
static void
|
||||
snapshot(Drawcmd *dc)
|
||||
{
|
||||
@@ -192,7 +210,7 @@ snapshot(Drawcmd *dc)
|
||||
|
||||
memset(dc, 0, sizeof *dc);
|
||||
if(search.lang)
|
||||
pre = search.text;
|
||||
searchpre(&pre);
|
||||
else
|
||||
impre(&im, &pre);
|
||||
if(!clientpre)
|
||||
@@ -265,7 +283,8 @@ okuriletter(Rune r)
|
||||
{
|
||||
static char *rows[] = {
|
||||
"aあ", "iい", "uう", "eえ", "oお",
|
||||
"kかきくけこ", "gがぎぐげご", "sさしすせそ", "zざじずぜぞ",
|
||||
"kかきくけこ", "gがぎぐげご", "sさしすせそ", "zざずぜぞ",
|
||||
"jじ",
|
||||
"tたちつてとっ", "dだぢづでど", "nなにぬねのん", "hはひふへほ",
|
||||
"bばびぶべぼ", "pぱぴぷぺぽ", "mまみむめも", "yやゆよ",
|
||||
"rらりるれろ", "wわ",
|
||||
@@ -625,14 +644,25 @@ emojiquery(void)
|
||||
selectfirst();
|
||||
}
|
||||
|
||||
/* The keys typed since Ctrl+H go on composing from the seed. */
|
||||
/*
|
||||
* The keys typed since Ctrl+H go on composing from the seed. A reading
|
||||
* is also the start of the longer words it begins, so its own
|
||||
* conversions come first and theirs follow: 대한 answers 大寒 and, past
|
||||
* them, 大韓民國.
|
||||
*/
|
||||
static void
|
||||
hanjaquery(void)
|
||||
{
|
||||
transstr(im.l, &search.seed, &search.raw, &search.text);
|
||||
Str kouho[Maxkouho], tail;
|
||||
int i, n;
|
||||
|
||||
transstr(im.l, &search.seed, &search.raw, &tail);
|
||||
search.text = search.back;
|
||||
sappend(&search.text, &tail);
|
||||
clearkouho();
|
||||
im.nkouho = dictlookup(getlang(LangHANJA)->dict, &search.text,
|
||||
im.kouho, Maxkouho);
|
||||
n = dictprefix(getlang(LangHANJA)->dict, &search.text, kouho, Maxkouho);
|
||||
for(i = 0; i < n; i++)
|
||||
addkouho(&kouho[i]);
|
||||
selectfirst();
|
||||
}
|
||||
|
||||
@@ -673,6 +703,7 @@ reset(void)
|
||||
{
|
||||
sclear(&im.pre);
|
||||
sclear(&im.raw);
|
||||
modemark = 0;
|
||||
okuriat = -1;
|
||||
endsearch();
|
||||
}
|
||||
@@ -685,9 +716,12 @@ reset(void)
|
||||
static void
|
||||
flush(Str *com)
|
||||
{
|
||||
if(search.lang)
|
||||
sappend(com, &search.text);
|
||||
else if(im.sel >= 0)
|
||||
Str pre;
|
||||
|
||||
if(search.lang){
|
||||
searchpre(&pre);
|
||||
sappend(com, &pre);
|
||||
}else if(im.sel >= 0)
|
||||
sappend(com, &im.kouho[im.sel]);
|
||||
else
|
||||
commitim(&im, com);
|
||||
@@ -708,6 +742,46 @@ takelost(void *owner, Str *com)
|
||||
lostowner = nil;
|
||||
}
|
||||
|
||||
/* The syllables from i to the end of the client's text, then seed. */
|
||||
static void
|
||||
reachkey(int i, Str *seed, Str *key)
|
||||
{
|
||||
sclear(key);
|
||||
for(; i < surround.n; i++)
|
||||
sputr(key, surround.r[i]);
|
||||
sappend(key, seed);
|
||||
}
|
||||
|
||||
/*
|
||||
* Ctrl+H converts a word, and a word is committed one syllable at a
|
||||
* time: only its last syllable is still pending. So the reading reaches
|
||||
* back into the text the client already has, as far as the dictionary
|
||||
* still knows the whole of it -- 한자 converts as a word where the 한 is
|
||||
* the client's and only the 자 is ours. Nothing but the dictionary says
|
||||
* where to stop: a reading is syllables, so a key holding a space or a
|
||||
* Hanja leads nowhere and the reach ends there.
|
||||
*/
|
||||
static void
|
||||
reachback(Str *seed, Str *back)
|
||||
{
|
||||
Str key;
|
||||
Trie *t;
|
||||
int i, best;
|
||||
|
||||
t = getlang(LangHANJA)->dict;
|
||||
best = surround.n;
|
||||
for(i = surround.n - 1; i >= 0; i--){
|
||||
if(surround.n - i + seed->n > Maxrunes)
|
||||
break;
|
||||
reachkey(i, seed, &key);
|
||||
if(trienode(t, &key) >= 0)
|
||||
best = i;
|
||||
}
|
||||
sclear(back);
|
||||
for(i = best; i < surround.n; i++)
|
||||
sputr(back, surround.r[i]);
|
||||
}
|
||||
|
||||
/*
|
||||
* A search takes over the keys. Hanja converts the pending syllable, so
|
||||
* it becomes the query; anything else pending is committed.
|
||||
@@ -715,23 +789,30 @@ takelost(void *owner, Str *com)
|
||||
static void
|
||||
startsearch(int lang, Str *com)
|
||||
{
|
||||
Str seed;
|
||||
Str back, seed;
|
||||
|
||||
sclear(&back);
|
||||
sclear(&seed);
|
||||
if(lang == LangHANJA && !search.lang && im.sel < 0)
|
||||
if(lang == LangHANJA && !search.lang && im.sel < 0){
|
||||
impre(&im, &seed);
|
||||
else
|
||||
/* Nothing pending is no reading, and reaches into nothing. */
|
||||
if(seed.n != 0)
|
||||
reachback(&seed, &back);
|
||||
}else
|
||||
flush(com);
|
||||
reset();
|
||||
search.lang = lang;
|
||||
search.back = back;
|
||||
search.seed = seed;
|
||||
if(lang == LangHANJA)
|
||||
searchquery();
|
||||
}
|
||||
|
||||
/* The client keeps what the reading reached back into; we take it back. */
|
||||
static void
|
||||
picksearch(int n, Str *com)
|
||||
{
|
||||
takeback = search.back.n;
|
||||
sappend(com, &im.kouho[n]);
|
||||
endsearch();
|
||||
}
|
||||
@@ -769,6 +850,8 @@ searchkey(u32int ks, u32int mod, Str *com)
|
||||
if(ks == Kback){
|
||||
if(search.raw.n != 0)
|
||||
spopr(&search.raw);
|
||||
else if(search.back.n != 0)
|
||||
sclear(&search.back); /* the reach undone, in its turn */
|
||||
else if(search.seed.n != 0)
|
||||
spopr(&search.seed);
|
||||
else{
|
||||
@@ -841,6 +924,11 @@ transition(u32int ks, u32int mod, Str *com)
|
||||
break;
|
||||
case Kmuhenkan: ks = 't'; mod = Mctrl; break;
|
||||
}
|
||||
/* Ctrl+Backspace deletes a word, Ctrl+Tab changes tab: not ours. */
|
||||
if(ks >= Kspec && (mod & ~Mshift)){
|
||||
flush(com);
|
||||
return 0;
|
||||
}
|
||||
if(search.lang)
|
||||
return searchkey(ks, mod, com);
|
||||
n = movedelta(ks);
|
||||
@@ -858,12 +946,15 @@ transition(u32int ks, u32int mod, Str *com)
|
||||
if((ks == ' ' || ks == Ktab) && isjp(&im) && haspre(&im) &&
|
||||
!(mod & ~Mshift)){
|
||||
/* Space and Tab convert: they step through the candidates,
|
||||
* Katakana last; a reading with none is committed. */
|
||||
* Katakana last. Katakana mode converts nothing, so there
|
||||
* the key only commits, and then it is the application's:
|
||||
* the space between two Katakana words is a space. */
|
||||
katakouho();
|
||||
if(im.nkouho != 0)
|
||||
cyclekouho(mod & Mshift ? -1 : 1);
|
||||
else
|
||||
if(im.nkouho == 0){
|
||||
flush(com);
|
||||
return 0;
|
||||
}
|
||||
cyclekouho(mod & Mshift ? -1 : 1);
|
||||
return 1;
|
||||
}
|
||||
if(ks == Ktab || ks == Kret || (ks == Kesc && !isjp(&im))){
|
||||
@@ -881,6 +972,7 @@ transition(u32int ks, u32int mod, Str *com)
|
||||
else if(ks == Kesc)
|
||||
reset();
|
||||
else{
|
||||
okuriat = -1; /* the mark goes with the kana it marked */
|
||||
im.l->back(&im);
|
||||
if(haspre(&im))
|
||||
dictqjp();
|
||||
@@ -938,6 +1030,8 @@ init(void)
|
||||
activeowner = nil;
|
||||
lostowner = nil;
|
||||
clientpre = 0;
|
||||
sclear(&surround);
|
||||
takeback = 0;
|
||||
memset(&lastdraw, 0, sizeof lastdraw);
|
||||
}
|
||||
|
||||
@@ -956,6 +1050,7 @@ imhandlekey(Keyreq *kr)
|
||||
sclear(&res.commit);
|
||||
sclear(&res.preedit);
|
||||
res.eaten = 1;
|
||||
takeback = 0;
|
||||
switch(kr->op){
|
||||
case Keyrelease:
|
||||
takelost(kr->owner, &res.commit);
|
||||
@@ -981,6 +1076,7 @@ imhandlekey(Keyreq *kr)
|
||||
break;
|
||||
case Keypress:
|
||||
res.eaten = 0;
|
||||
takelost(kr->owner, &res.commit);
|
||||
if(kr->owner == activeowner)
|
||||
clientpre = kr->clientpre;
|
||||
if(keymeaningful(kr->ks)){
|
||||
@@ -992,6 +1088,7 @@ imhandlekey(Keyreq *kr)
|
||||
clientpre = kr->clientpre;
|
||||
}
|
||||
caret = kr->caret;
|
||||
surround = kr->surround;
|
||||
res.eaten = transition(kr->ks, kr->mod, &res.commit);
|
||||
}
|
||||
break;
|
||||
@@ -999,10 +1096,13 @@ imhandlekey(Keyreq *kr)
|
||||
redraw();
|
||||
if(kr->owner == activeowner){
|
||||
if(search.lang)
|
||||
res.preedit = search.text;
|
||||
searchpre(&res.preedit);
|
||||
else if(im.sel >= 0)
|
||||
res.preedit = im.kouho[im.sel]; /* what Enter would commit */
|
||||
else
|
||||
impre(&im, &res.preedit);
|
||||
}
|
||||
res.del = takeback;
|
||||
chansend(kr->reply, &res);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ HOSTCC = cc
|
||||
PKG_CONFIG ?= pkg-config
|
||||
CFLAGS ?= -O2 -g
|
||||
UNIT_CPPFLAGS = -I..
|
||||
UNIT_CFLAGS = -std=c99 -Wall -Wextra
|
||||
UNIT_CFLAGS = -Wall -Wextra
|
||||
HOST_CFLAGS = -std=c99 -Wall -Wextra
|
||||
TEXT_CFLAGS = $(shell $(PKG_CONFIG) --cflags pangocairo cairo fontconfig)
|
||||
TEXT_LIBS = $(shell $(PKG_CONFIG) --libs pangocairo cairo fontconfig)
|
||||
@@ -20,7 +20,9 @@ X11_CFLAGS = $(shell $(PKG_CONFIG) --cflags x11)
|
||||
X11_LIBS = $(shell $(PKG_CONFIG) --libs x11)
|
||||
XIM_CFLAGS = $(shell $(PKG_CONFIG) --cflags xcb-imdkit xcb-aux xcb-xkb xkbcommon-x11)
|
||||
XIM_LIBS = $(shell $(PKG_CONFIG) --libs xcb-imdkit xcb-aux xcb-xkb xkbcommon-x11)
|
||||
UNIT_LDLIBS = -lthread -lbio $(TEXT_LIBS) $(IBUS_LIBS) $(XIM_LIBS)
|
||||
WL_CFLAGS = $(shell $(PKG_CONFIG) --cflags wayland-client xkbcommon)
|
||||
WL_LIBS = $(shell $(PKG_CONFIG) --libs wayland-client xkbcommon)
|
||||
UNIT_LDLIBS = -lthread -lbio $(TEXT_LIBS) $(IBUS_LIBS) $(XIM_LIBS) $(WL_LIBS)
|
||||
|
||||
PROG = unit_test
|
||||
STRESS = stress_test
|
||||
@@ -33,10 +35,11 @@ LIVE = $(SMOKE) $(FAULT)
|
||||
TESTSRC = test_util.c str_test.c trie_test.c \
|
||||
ko_test.c vi_test.c engine_test.c dict_test.c ipc_test.c \
|
||||
popup_test.c font_test.c ibus_test.c server_test.c compose_test.c \
|
||||
xim_adapter_test.c
|
||||
xim_adapter_test.c wl_adapter_test.c
|
||||
TESTOBJ = $(TESTSRC:.c=.o)
|
||||
# imv2.c and vkv1.c are wayland-scanner's; the parent Makefile writes them.
|
||||
PARENTSRC = str.c trie.c dict.c ko.c vi.c ipc.c popup_layout.c \
|
||||
font.c compose.c
|
||||
font.c compose.c imv2.c vkv1.c
|
||||
PARENTOBJ = $(PARENTSRC:%.c=unit_%.o)
|
||||
COMMONOBJ = $(TESTOBJ) $(PARENTOBJ)
|
||||
OBJS = unit_test.o stress_test.o $(COMMONOBJ)
|
||||
@@ -86,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) \
|
||||
@@ -124,6 +131,8 @@ ibus_test.o: UNIT_CPPFLAGS += $(IBUS_CFLAGS)
|
||||
ibus_test.o: ../ibus.c
|
||||
xim_adapter_test.o: UNIT_CPPFLAGS += $(XIM_CFLAGS)
|
||||
xim_adapter_test.o: ../xim.c
|
||||
wl_adapter_test.o unit_imv2.o unit_vkv1.o: UNIT_CPPFLAGS += $(WL_CFLAGS)
|
||||
wl_adapter_test.o: ../wl.c ../imv2.h ../vkv1.h
|
||||
server_test.o: ../srv.c
|
||||
unit_font.o: UNIT_CPPFLAGS += $(TEXT_CFLAGS)
|
||||
unit_compose.o compose_test.o: UNIT_CPPFLAGS += $(IBUS_CFLAGS)
|
||||
@@ -137,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
|
||||
|
||||
@@ -3,24 +3,37 @@
|
||||
#include "test.h"
|
||||
#include <xkbcommon/xkbcommon-keysyms.h>
|
||||
|
||||
/* A sequence swallows its keys and hands its text back at the end. */
|
||||
/*
|
||||
* A sequence swallows its keys and hands its text back at the end, and
|
||||
* belongs to the one context, in the one frontend, that started it.
|
||||
*/
|
||||
void
|
||||
compose_sequences(struct ct *t)
|
||||
{
|
||||
static int a, b; /* two input contexts, told apart by address */
|
||||
static const struct {
|
||||
int who;
|
||||
void *owner;
|
||||
u32int sym;
|
||||
int composing;
|
||||
char *text;
|
||||
} keys[] = {
|
||||
{ XKB_KEY_a, 0, "" },
|
||||
{ XKB_KEY_dead_acute, 1, "" },
|
||||
{ XKB_KEY_e, 0, "é" },
|
||||
{ XKB_KEY_Multi_key, 1, "" },
|
||||
{ XKB_KEY_o, 1, "" },
|
||||
{ XKB_KEY_c, 0, "©" },
|
||||
{ XKB_KEY_dead_acute, 1, "" },
|
||||
{ XKB_KEY_Escape, 1, "" },
|
||||
{ XKB_KEY_a, 0, "" },
|
||||
{ Composexim, &a, XKB_KEY_a, 0, "" },
|
||||
{ Composexim, &a, XKB_KEY_dead_acute, 1, "" },
|
||||
{ Composexim, &a, XKB_KEY_e, 0, "é" },
|
||||
{ Composexim, &a, XKB_KEY_Multi_key, 1, "" },
|
||||
{ Composexim, &a, XKB_KEY_o, 1, "" },
|
||||
{ Composexim, &a, XKB_KEY_c, 0, "©" },
|
||||
{ Composexim, &a, XKB_KEY_dead_acute, 1, "" },
|
||||
{ Composexim, &a, XKB_KEY_Escape, 1, "" },
|
||||
{ Composexim, &a, XKB_KEY_a, 0, "" },
|
||||
/* another context of the same frontend starts over */
|
||||
{ Composexim, &a, XKB_KEY_dead_acute, 1, "" },
|
||||
{ Composexim, &b, XKB_KEY_e, 0, "" },
|
||||
/* another frontend does not touch this one's sequence */
|
||||
{ Composexim, &a, XKB_KEY_dead_acute, 1, "" },
|
||||
{ Composewl, &a, XKB_KEY_o, 0, "" },
|
||||
{ Composexim, &a, XKB_KEY_e, 0, "é" },
|
||||
};
|
||||
char buf[Maxutf];
|
||||
int i;
|
||||
@@ -29,9 +42,15 @@ compose_sequences(struct ct *t)
|
||||
return;
|
||||
composeinit();
|
||||
for(i = 0; i < nelem(keys); i++){
|
||||
CT_EQ_INT(t, keys[i].composing,
|
||||
composekey(keys[i].sym, buf, sizeof buf));
|
||||
CT_EQ_INT(t, keys[i].composing, composekey(keys[i].who,
|
||||
keys[i].owner, keys[i].sym, buf, sizeof buf));
|
||||
CT_EQ_STR(t, keys[i].text, buf);
|
||||
}
|
||||
/* a context that goes takes its half-typed sequence with it */
|
||||
CT_EQ_INT(t, 1,
|
||||
composekey(Composewl, &a, XKB_KEY_dead_acute, buf, sizeof buf));
|
||||
composedrop(Composewl, &a);
|
||||
CT_EQ_INT(t, 0, composekey(Composewl, &a, XKB_KEY_e, buf, sizeof buf));
|
||||
CT_EQ_STR(t, "", buf);
|
||||
unsetenv("XCOMPOSEFILE");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ keystroke(u32int ks, u32int mod, Str *com)
|
||||
{
|
||||
int eaten;
|
||||
|
||||
takeback = 0;
|
||||
eaten = keymeaningful(ks) && transition(ks, mod, com);
|
||||
redraw();
|
||||
return eaten;
|
||||
@@ -175,7 +176,7 @@ engine_candidate_shortcut_modifiers(struct ct *t)
|
||||
|
||||
static Keyres
|
||||
ownerrequestatcap(void *owner, int cap, int op, Rune key, u32int mod,
|
||||
Caret *caret)
|
||||
Caret *caret, char *before)
|
||||
{
|
||||
Channel *reply;
|
||||
Keyreq req;
|
||||
@@ -190,6 +191,8 @@ ownerrequestatcap(void *owner, int cap, int op, Rune key, u32int mod,
|
||||
req.mod = mod;
|
||||
if(caret != nil)
|
||||
req.caret = *caret;
|
||||
if(before != nil)
|
||||
req.surround = mkstr(before);
|
||||
req.reply = reply;
|
||||
imhandlekey(&req);
|
||||
chanrecv(reply, &res);
|
||||
@@ -200,7 +203,7 @@ ownerrequestatcap(void *owner, int cap, int op, Rune key, u32int mod,
|
||||
static Keyres
|
||||
ownerrequestat(void *owner, int op, Rune key, u32int mod, Caret *caret)
|
||||
{
|
||||
return ownerrequestatcap(owner, 0, op, key, mod, caret);
|
||||
return ownerrequestatcap(owner, 0, op, key, mod, caret, nil);
|
||||
}
|
||||
|
||||
static Keyres
|
||||
@@ -212,7 +215,7 @@ ownerrequest(void *owner, int op, Rune key, u32int mod)
|
||||
static Keyres
|
||||
ownerrequestcap(void *owner, int cap, int op, Rune key, u32int mod)
|
||||
{
|
||||
return ownerrequestatcap(owner, cap, op, key, mod, nil);
|
||||
return ownerrequestatcap(owner, cap, op, key, mod, nil, nil);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -258,6 +261,20 @@ engine_active_owner_lifecycle(struct ct *t)
|
||||
res = ownerrequest(&c, Keypress, 'k', 0);
|
||||
CT_EQ_PTR(t, &c, activeowner);
|
||||
checkstr(t, "next owner first key", "k", &res.preedit);
|
||||
|
||||
/* Typing is being heard from, as much as a reset or a release is. */
|
||||
init();
|
||||
im.l = getlang(LangJP);
|
||||
ownerrequest(&a, Keypress, 'k', 0);
|
||||
ownerrequest(&a, Keypress, 'a', 0);
|
||||
ownerrequest(&b, Keypress, 'n', 0);
|
||||
CT_EQ_PTR(t, &b, activeowner);
|
||||
res = ownerrequest(&a, Keypress, 'k', 0);
|
||||
checkstr(t, "the taken owner types and gets its reading back", "か",
|
||||
&res.commit);
|
||||
CT_EQ_PTR(t, &a, activeowner);
|
||||
res = ownerrequest(&b, Keyreset, 0, 0);
|
||||
checkstr(t, "the owner it took from keeps its own", "ん", &res.commit);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -394,6 +411,7 @@ engine_commit_contract(struct ct *t)
|
||||
{ 'd', "ㅇ", "ㅇㅇㅇ" },
|
||||
{ 'k', "ㅏ", "ㅏㅏㅏ" },
|
||||
};
|
||||
Drawcmd dc;
|
||||
Keyres res;
|
||||
Str all, com;
|
||||
char owner;
|
||||
@@ -478,6 +496,26 @@ engine_commit_contract(struct ct *t)
|
||||
CT_CHECK(t, !keystroke(0xf008, 0, &com));
|
||||
checkstr(t, "non-ASCII key commit", "か", &com);
|
||||
CT_EQ_INT(t, 0, im.pre.n);
|
||||
|
||||
/* A chosen candidate is what the client shows and what Enter
|
||||
* commits; the popup keeps the reading above the list. */
|
||||
init();
|
||||
im.l = getlang(LangJP);
|
||||
draindraw(nil);
|
||||
ownerrequestcap(&owner, 1, Keypress, 'k', 0);
|
||||
res = ownerrequestcap(&owner, 1, Keypress, 'a', 0);
|
||||
checkstr(t, "the reading before a candidate", "か", &res.preedit);
|
||||
im.kouho[0] = mkstr("蚊");
|
||||
im.nkouho = 1;
|
||||
res = ownerrequestcap(&owner, 1, Keypress, Kdown, 0);
|
||||
checkstr(t, "the candidate the client shows", "蚊", &res.preedit);
|
||||
clientpre = 0;
|
||||
redraw();
|
||||
if(CT_CHECK(t, draindraw(&dc) > 0))
|
||||
checkstr(t, "the popup keeps the reading", "か", &dc.pre);
|
||||
res = ownerrequestcap(&owner, 1, Keypress, Kret, 0);
|
||||
checkstr(t, "Enter commits what was shown", "蚊", &res.commit);
|
||||
CT_EQ_INT(t, 0, res.preedit.n);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -560,6 +598,15 @@ engine_korean_modifiers_and_backspace(struct ct *t)
|
||||
CT_EQ_INT(t, 0, im.pre.n);
|
||||
CT_CHECK(t, !keystroke(Kback, 0, &com));
|
||||
}
|
||||
|
||||
init();
|
||||
im.l = getlang(LangKO);
|
||||
sclear(&com);
|
||||
if(typekeys(t, "rk", &com)){
|
||||
CT_CHECK(t, !keystroke(Kback, Mctrl, &com));
|
||||
checkstr(t, "Ctrl+Backspace commits and passes", "가", &com);
|
||||
CT_EQ_INT(t, 0, im.pre.n);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -567,6 +614,7 @@ engine_direct_language_modes(struct ct *t)
|
||||
{
|
||||
Drawcmd dc;
|
||||
Str com;
|
||||
char owner;
|
||||
|
||||
init();
|
||||
draindraw(nil);
|
||||
@@ -607,6 +655,16 @@ engine_direct_language_modes(struct ct *t)
|
||||
checkstr(t, "Korean without map", "ㄱ", &im.pre);
|
||||
CT_CHECK(t, keystroke(Kback, 0, &com));
|
||||
CT_EQ_INT(t, 0, im.pre.n);
|
||||
|
||||
/* Leaving takes the mark away as surely as typing does. */
|
||||
init();
|
||||
draindraw(nil);
|
||||
ownerrequest(&owner, Keypress, 's', Mctrl);
|
||||
if(CT_CHECK(t, draindraw(&dc) > 0))
|
||||
checkstr(t, "mode mark", "한", &dc.pre);
|
||||
ownerrequest(&owner, Keyrelease, 0, 0);
|
||||
if(CT_CHECK(t, draindraw(&dc) > 0))
|
||||
CT_EQ_INT(t, 0, dc.pre.n);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -837,6 +895,10 @@ engine_candidate_page_movement(struct ct *t)
|
||||
{ 32, 22, Kpgdown, 31 },
|
||||
{ 32, 31, Kpgup, 22 },
|
||||
{ 32, 31, Kpgdown, 31 },
|
||||
{ 32, -1, Kdown, 0 },
|
||||
{ 32, -1, Kup, 0 },
|
||||
{ 32, -1, Kpgdown, 9 },
|
||||
{ 32, -1, Kpgup, 0 },
|
||||
};
|
||||
int first, i, n;
|
||||
Str com;
|
||||
@@ -1066,6 +1128,8 @@ hanjabegin(Searchfix *f, int lang)
|
||||
f->dictlang->dict = trienew();
|
||||
setdict(f->dictlang->dict, "한", "漢 韓");
|
||||
setdict(f->dictlang->dict, "가", "家");
|
||||
setdict(f->dictlang->dict, "민", "民");
|
||||
setdict(f->dictlang->dict, "민국", "民國");
|
||||
init();
|
||||
im.l = getlang(lang);
|
||||
}
|
||||
@@ -1234,6 +1298,13 @@ engine_japanese_candidates(struct ct *t)
|
||||
checkstr(t, "then the second", "描く", &im.kouho[1]);
|
||||
checkstr(t, "then the reading's own", "確", &im.kouho[2]);
|
||||
}
|
||||
/* Backspace edits the reading, so the split it marked is gone. */
|
||||
CT_CHECK(t, keystroke(Kback, 0, &com));
|
||||
CT_CHECK(t, keystroke('k', 0, &com));
|
||||
CT_CHECK(t, keystroke('u', 0, &com));
|
||||
if(CT_EQ_INT(t, 3, im.nkouho))
|
||||
checkstr(t, "the mark did not outlive its kana", "確",
|
||||
&im.kouho[0]);
|
||||
CT_CHECK(t, keystroke(Kesc, 0, &com));
|
||||
setdict(jp->dict, "いt", "言 行");
|
||||
setdict(jp->dict, "いっt", "言 行");
|
||||
@@ -1244,6 +1315,19 @@ engine_japanese_candidates(struct ct *t)
|
||||
checkstr(t, "sokuon written in kana", "行った", &im.kouho[1]);
|
||||
}
|
||||
CT_CHECK(t, keystroke(Kesc, 0, &com));
|
||||
/* じ is keyed j, as SKK types it; ざずぜぞ keep z. */
|
||||
setdict(jp->dict, "しんj", "信");
|
||||
setdict(jp->dict, "まz", "混");
|
||||
if(!typekeys(t, "shinjiru", &com))
|
||||
goto cleanup;
|
||||
if(CT_EQ_INT(t, 1, im.nkouho))
|
||||
checkstr(t, "じ okurigana", "信じる", &im.kouho[0]);
|
||||
CT_CHECK(t, keystroke(Kesc, 0, &com));
|
||||
if(!typekeys(t, "mazeru", &com))
|
||||
goto cleanup;
|
||||
if(CT_EQ_INT(t, 1, im.nkouho))
|
||||
checkstr(t, "ぜ okurigana", "混ぜる", &im.kouho[0]);
|
||||
CT_CHECK(t, keystroke(Kesc, 0, &com));
|
||||
/* A romaji typo stays in the reading for Backspace to mend. */
|
||||
if(!typekeys(t, "kanjr", &com))
|
||||
goto cleanup;
|
||||
@@ -1388,6 +1472,16 @@ engine_katakana_sequences(struct ct *t)
|
||||
checkstr(t, "committed Katakana", cases[i].want, &com);
|
||||
}
|
||||
CT_EQ_PTR(t, nil, getlang(LangJPK)->dictname);
|
||||
|
||||
/* Nothing to convert to: the key commits and goes on to the client. */
|
||||
init();
|
||||
im.l = getlang(LangJPK);
|
||||
sclear(&com);
|
||||
if(typekeys(t, "kaku", &com)){
|
||||
CT_CHECK(t, !keystroke(' ', 0, &com));
|
||||
checkstr(t, "Space commits the Katakana", "カク", &com);
|
||||
CT_EQ_INT(t, 0, im.nkouho);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -1882,6 +1976,140 @@ cleanup:
|
||||
searchend(&f);
|
||||
}
|
||||
|
||||
/*
|
||||
* A word is committed one syllable at a time, so Ctrl+H reaches back
|
||||
* into the text the client already has, and a pick takes it back.
|
||||
*/
|
||||
void
|
||||
engine_hanja_reaches_back(struct ct *t)
|
||||
{
|
||||
Searchfix f;
|
||||
Keyres res;
|
||||
Str com;
|
||||
char owner;
|
||||
|
||||
hanjabegin(&f, LangKO);
|
||||
surround = mkstr("민");
|
||||
im.pre = mkstr("국");
|
||||
sclear(&com);
|
||||
if(!CT_CHECK(t, keystroke('h', Mctrl, &com)))
|
||||
goto cleanup;
|
||||
checkstr(t, "the client's syllable joined the reading", "민국",
|
||||
&search.text);
|
||||
checkstr(t, "what the client already has", "민", &search.back);
|
||||
CT_EQ_INT(t, 1, im.nkouho);
|
||||
CT_CHECK(t, keystroke(Kret, 0, &com));
|
||||
checkstr(t, "the word converted", "民國", &com);
|
||||
CT_EQ_INT(t, 1, takeback);
|
||||
|
||||
/* Escape gives back what was pending, and nothing of the client's. */
|
||||
surround = mkstr("민");
|
||||
im.pre = mkstr("국");
|
||||
sclear(&com);
|
||||
CT_CHECK(t, keystroke('h', Mctrl, &com));
|
||||
CT_CHECK(t, keystroke(Kesc, 0, &com));
|
||||
checkstr(t, "only the pending syllable comes back", "국", &im.pre);
|
||||
CT_EQ_INT(t, 0, takeback);
|
||||
|
||||
/* A reading nothing is keyed by stops the reach where it stands. */
|
||||
surround = mkstr("가");
|
||||
im.pre = mkstr("국");
|
||||
sclear(&com);
|
||||
CT_CHECK(t, keystroke('h', Mctrl, &com));
|
||||
checkstr(t, "no reach without a reading", "국", &search.text);
|
||||
CT_EQ_INT(t, 0, search.back.n);
|
||||
CT_CHECK(t, keystroke(Kesc, 0, &com));
|
||||
|
||||
/* A query without candidates types only what the client lacks. */
|
||||
surround = mkstr("민");
|
||||
im.pre = mkstr("국");
|
||||
sclear(&com);
|
||||
CT_CHECK(t, keystroke('h', Mctrl, &com));
|
||||
if(typekeys(t, "r", &com)){
|
||||
CT_EQ_INT(t, 0, im.nkouho);
|
||||
CT_CHECK(t, keystroke(Kret, 0, &com));
|
||||
checkstr(t, "the client's syllable is not typed again", "국ㄱ",
|
||||
&com);
|
||||
CT_EQ_INT(t, 0, takeback);
|
||||
}
|
||||
|
||||
/* Backspace gives the reach back before it eats what was typed. */
|
||||
surround = mkstr("민");
|
||||
im.pre = mkstr("국");
|
||||
sclear(&com);
|
||||
CT_CHECK(t, keystroke('h', Mctrl, &com));
|
||||
checkstr(t, "the reach joined the reading", "민국", &search.text);
|
||||
CT_CHECK(t, keystroke(Kback, 0, &com));
|
||||
checkstr(t, "the client's syllable goes back first", "국",
|
||||
&search.text);
|
||||
CT_EQ_INT(t, 0, search.back.n);
|
||||
sclear(&com);
|
||||
CT_CHECK(t, keystroke(Kret, 0, &com));
|
||||
checkstr(t, "only what was typed is committed", "국", &com);
|
||||
CT_EQ_INT(t, 0, takeback);
|
||||
|
||||
/* Nothing pending is no reading, and reaches into nothing. */
|
||||
surround = mkstr("민");
|
||||
sclear(&im.pre);
|
||||
sclear(&com);
|
||||
CT_CHECK(t, keystroke('h', Mctrl, &com));
|
||||
CT_EQ_INT(t, 0, search.back.n);
|
||||
CT_EQ_INT(t, 0, im.nkouho);
|
||||
if(typekeys(t, "als", &com)){
|
||||
checkstr(t, "the reading typed is the whole query", "민",
|
||||
&search.text);
|
||||
CT_CHECK(t, keystroke(Kret, 0, &com));
|
||||
checkstr(t, "the client's syllable is not converted", "民",
|
||||
&com);
|
||||
CT_EQ_INT(t, 0, takeback);
|
||||
}
|
||||
|
||||
/* The frontend's own request carries the text and is told to take it. */
|
||||
init();
|
||||
im.l = getlang(LangKO);
|
||||
ownerrequestatcap(&owner, 1, Keypress, 'r', 0, nil, "민");
|
||||
ownerrequestatcap(&owner, 1, Keypress, 'n', 0, nil, "민");
|
||||
res = ownerrequestatcap(&owner, 1, Keypress, 'r', 0, nil, "민");
|
||||
checkstr(t, "the syllable the request composed", "국", &res.preedit);
|
||||
CT_EQ_INT(t, 0, res.del);
|
||||
ownerrequestatcap(&owner, 1, Keypress, 'h', Mctrl, nil, "민");
|
||||
checkstr(t, "the request's own text joined the reading", "민국",
|
||||
&search.text);
|
||||
res = ownerrequestatcap(&owner, 1, Keypress, Kret, 0, nil, "민");
|
||||
checkstr(t, "the word the request converted", "民國", &res.commit);
|
||||
CT_EQ_INT(t, 1, res.del);
|
||||
cleanup:
|
||||
searchend(&f);
|
||||
}
|
||||
|
||||
/*
|
||||
* A reading is the start of the longer words it begins: its own
|
||||
* conversions come first, and past them theirs.
|
||||
*/
|
||||
void
|
||||
engine_hanja_word_prefix(struct ct *t)
|
||||
{
|
||||
Searchfix f;
|
||||
Str com;
|
||||
|
||||
hanjabegin(&f, LangKO);
|
||||
/* Two readings under one prefix may share a word; it shows once. */
|
||||
setdict(f.dictlang->dict, "민민", "民");
|
||||
sclear(&com);
|
||||
if(!CT_CHECK(t, keystroke('h', Mctrl, &com)) ||
|
||||
!typekeys(t, "als", &com))
|
||||
goto cleanup;
|
||||
checkstr(t, "the reading typed so far", "민", &search.text);
|
||||
CT_EQ_INT(t, 2, im.nkouho);
|
||||
checkstr(t, "the reading's own conversion", "民", &im.kouho[0]);
|
||||
checkstr(t, "the word it begins", "民國", &im.kouho[1]);
|
||||
CT_CHECK(t, keystroke(Kdown, 0, &com));
|
||||
CT_CHECK(t, keystroke(Kret, 0, &com));
|
||||
checkstr(t, "a word longer than the reading", "民國", &com);
|
||||
cleanup:
|
||||
searchend(&f);
|
||||
}
|
||||
|
||||
void
|
||||
engine_hanja_korean_keys(struct ct *t)
|
||||
{
|
||||
|
||||
@@ -32,6 +32,7 @@ enum
|
||||
Ecaret,
|
||||
Ekey,
|
||||
Ereset,
|
||||
Esurround,
|
||||
};
|
||||
|
||||
enum
|
||||
@@ -55,6 +56,7 @@ struct Event
|
||||
int32_t x;
|
||||
int32_t y;
|
||||
int32_t h;
|
||||
char text[Ipcfieldmax+1];
|
||||
};
|
||||
|
||||
struct Server
|
||||
@@ -69,6 +71,7 @@ struct Server
|
||||
int eaten;
|
||||
int stall;
|
||||
int response;
|
||||
int del;
|
||||
int nclose;
|
||||
char pre[Ipcfieldmax+1];
|
||||
Event ev[Maxevent];
|
||||
@@ -83,6 +86,10 @@ struct Siglog
|
||||
char shown[Ipcfieldmax+1];
|
||||
char commit[Ipcfieldmax+1];
|
||||
int prechanged;
|
||||
char around[64]; /* what the widget answers retrieve-surrounding */
|
||||
int aroundcursor;
|
||||
int deloffset;
|
||||
int deln;
|
||||
};
|
||||
|
||||
static void
|
||||
@@ -110,13 +117,14 @@ sendresponse(Server *s, int fd, int eaten, int want, int key)
|
||||
unsigned char buf[Ipcmaxresp];
|
||||
const char *commit;
|
||||
char pre[Ipcfieldmax+1];
|
||||
int n, response, stall;
|
||||
int del, n, response, stall;
|
||||
size_t ncommit, npreedit;
|
||||
|
||||
pthread_mutex_lock(&s->lock);
|
||||
memcpy(pre, s->pre, sizeof pre);
|
||||
response = s->response;
|
||||
stall = s->stall;
|
||||
del = key ? s->del : 0;
|
||||
pthread_mutex_unlock(&s->lock);
|
||||
if(stall)
|
||||
return 1;
|
||||
@@ -135,7 +143,7 @@ sendresponse(Server *s, int fd, int eaten, int want, int key)
|
||||
pre[2] = 'y';
|
||||
npreedit = 3;
|
||||
}
|
||||
n = ipcpackresp(buf, sizeof buf, eaten, commit, ncommit,
|
||||
n = ipcpackresp(buf, sizeof buf, eaten, del, commit, ncommit,
|
||||
pre, npreedit, want);
|
||||
return n >= 0 && ipcsend(fd, buf, n) == 0;
|
||||
}
|
||||
@@ -146,6 +154,7 @@ request(Server *s, int fd)
|
||||
unsigned char buf[Ipccaretsz];
|
||||
uint32_t mod, key;
|
||||
Event e;
|
||||
size_t n;
|
||||
int old, type, want;
|
||||
|
||||
if(ipcreadn(fd, buf, Ipcreqsz) < 0)
|
||||
@@ -168,6 +177,14 @@ request(Server *s, int fd)
|
||||
e.type = Ecaret;
|
||||
record(s, &e);
|
||||
return 1;
|
||||
case Ipcsurround:
|
||||
n = ipcsurroundlen(buf);
|
||||
if(n > 0 && ipcreadn(fd, e.text, n) < 0)
|
||||
return 0;
|
||||
e.text[n] = '\0';
|
||||
e.type = Esurround;
|
||||
record(s, &e);
|
||||
return 1;
|
||||
case Ipckey:
|
||||
ipcunpackreq(buf, &want, &mod, &key);
|
||||
e.want = want;
|
||||
@@ -513,6 +530,25 @@ clearlog(Siglog *l)
|
||||
memset(l, 0, sizeof *l);
|
||||
}
|
||||
|
||||
/* The widget's half of the surrounding-text contract. */
|
||||
static gboolean
|
||||
retrievearound(GtkIMContext *ctx, Siglog *l)
|
||||
{
|
||||
if(l->around[0] == '\0')
|
||||
return FALSE;
|
||||
gtk_im_context_set_surrounding(ctx, l->around, -1, l->aroundcursor);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
deletearound(GtkIMContext *ctx, gint offset, gint n, Siglog *l)
|
||||
{
|
||||
(void)ctx;
|
||||
l->deloffset = offset;
|
||||
l->deln = n;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static GtkIMContext*
|
||||
newcontext(Siglog *l)
|
||||
{
|
||||
@@ -520,6 +556,8 @@ newcontext(Siglog *l)
|
||||
|
||||
ctx = gtk_im_multicontext_new();
|
||||
gtk_im_multicontext_set_context_id(GTK_IM_MULTICONTEXT(ctx), "strans");
|
||||
g_signal_connect(ctx, "retrieve-surrounding", G_CALLBACK(retrievearound), l);
|
||||
g_signal_connect(ctx, "delete-surrounding", G_CALLBACK(deletearound), l);
|
||||
g_signal_connect(ctx, "preedit-start", G_CALLBACK(prestart), l);
|
||||
g_signal_connect(ctx, "preedit-changed", G_CALLBACK(prechange), l);
|
||||
g_signal_connect(ctx, "preedit-end", G_CALLBACK(preend), l);
|
||||
@@ -570,6 +608,25 @@ newwindow(int x, int y)
|
||||
return w;
|
||||
}
|
||||
|
||||
/* A GtkEntry gives its context a window of its own, under the toplevel's. */
|
||||
static GdkWindow*
|
||||
entrywindow(GtkWidget *entry)
|
||||
{
|
||||
GdkWindow *w;
|
||||
GList *kids, *l;
|
||||
gpointer u;
|
||||
|
||||
w = NULL;
|
||||
kids = gdk_window_get_children(gtk_widget_get_window(entry));
|
||||
for(l = kids; l != NULL; l = l->next){
|
||||
gdk_window_get_user_data(l->data, &u);
|
||||
if(u == entry)
|
||||
w = l->data;
|
||||
}
|
||||
g_list_free(kids);
|
||||
return w;
|
||||
}
|
||||
|
||||
#define Check(c, ...) do{ if(!(c)){ fail(__VA_ARGS__); goto out; } }while(0)
|
||||
|
||||
int
|
||||
@@ -578,8 +635,8 @@ main(int argc, char **argv)
|
||||
Server srv;
|
||||
Siglog log;
|
||||
GtkIMContext *ctx, *oldctx, *badctx;
|
||||
GtkWidget *top;
|
||||
GdkWindow *win;
|
||||
GtkWidget *top, *entry;
|
||||
GdkWindow *win, *entrywin;
|
||||
GdkRectangle rect;
|
||||
Event e;
|
||||
static const GtkInputPurpose purposes[] = {
|
||||
@@ -648,7 +705,7 @@ main(int argc, char **argv)
|
||||
Check(eventcount(&srv) == 0, "cursor reporting opened the socket");
|
||||
setpre(&srv, "");
|
||||
Check(key(ctx, win, GDK_KEY_a), "initial key was not eaten");
|
||||
Check(waitcount(&srv, 3), "initial protocol frames timed out");
|
||||
Check(waitcount(&srv, 4), "initial protocol frames timed out");
|
||||
e = getevent(&srv, 0);
|
||||
Check(e.type == Ecap && e.want == 1, "default preedit capability missing");
|
||||
e = getevent(&srv, 1);
|
||||
@@ -663,6 +720,9 @@ main(int argc, char **argv)
|
||||
e.x, e.y, e.h, (ox + rect.x) * scale,
|
||||
(oy + rect.y) * scale, rect.height * scale);
|
||||
e = getevent(&srv, 2);
|
||||
Check(e.type == Esurround && e.text[0] == '\0',
|
||||
"a widget that keeps no text sent some");
|
||||
e = getevent(&srv, 3);
|
||||
Check(e.type == Ekey && e.want == 1, "initial key framing changed");
|
||||
Check(log.n == 0, "initial empty preedit emitted %s", log.event);
|
||||
setpre(&srv, "ㅋ");
|
||||
@@ -757,6 +817,66 @@ main(int argc, char **argv)
|
||||
}
|
||||
|
||||
g_object_set(ctx, "input-purpose", GTK_INPUT_PURPOSE_FREE_FORM, NULL);
|
||||
|
||||
/* An entry hidden the older way is private too, purpose or no purpose. */
|
||||
entry = gtk_entry_new();
|
||||
gtk_container_add(GTK_CONTAINER(top), entry);
|
||||
gtk_widget_show(entry);
|
||||
pump();
|
||||
entrywin = entrywindow(entry);
|
||||
Check(entrywin != NULL, "the test GtkEntry has no window of its own");
|
||||
first = eventcount(&srv);
|
||||
gtk_im_context_set_client_window(ctx, entrywin);
|
||||
/* The caret the new window invalidates must land before keys are counted. */
|
||||
Check(waitcount(&srv, first + 1) && getevent(&srv, first).type == Ecaret,
|
||||
"the entry window did not renegotiate the caret");
|
||||
setpre(&srv, "");
|
||||
gtk_entry_set_visibility(GTK_ENTRY(entry), FALSE);
|
||||
clearlog(&log);
|
||||
first = eventcount(&srv);
|
||||
Check(key(ctx, entrywin, GDK_KEY_y), "hidden-entry fallback did not commit");
|
||||
Check(eventcount(&srv) == first && strcmp(log.commit, "y") == 0,
|
||||
"hidden-entry key reached daemon or did not commit");
|
||||
gtk_entry_set_visibility(GTK_ENTRY(entry), TRUE);
|
||||
clearlog(&log);
|
||||
first = eventcount(&srv);
|
||||
Check(key(ctx, entrywin, GDK_KEY_y), "visible-entry key was not eaten");
|
||||
Check(waitcount(&srv, first + 1) && log.commit[0] == '\0',
|
||||
"visible-entry key did not reach the daemon");
|
||||
gtk_im_context_set_client_window(ctx, win);
|
||||
|
||||
/* The widget's own text goes over, and a take-back comes back. */
|
||||
setpre(&srv, "");
|
||||
clearlog(&log);
|
||||
snprintf(log.around, sizeof log.around, "%s", "가나한");
|
||||
log.aroundcursor = 9;
|
||||
first = eventcount(&srv);
|
||||
Check(key(ctx, win, GDK_KEY_j), "surrounding-text key was not eaten");
|
||||
Check(waitcount(&srv, first + 2), "surrounding text did not reach the daemon");
|
||||
e = getevent(&srv, first);
|
||||
Check(e.type == Esurround && strcmp(e.text, "가나한") == 0,
|
||||
"the daemon was sent %s, not the text before the cursor", e.text);
|
||||
Check(getevent(&srv, first + 1).type == Ekey,
|
||||
"the text did not ride ahead of the key");
|
||||
/* Sent again only when it changes. */
|
||||
first = eventcount(&srv);
|
||||
Check(key(ctx, win, GDK_KEY_j), "second surrounding-text key was not eaten");
|
||||
Check(waitcount(&srv, first + 1) && getevent(&srv, first).type == Ekey,
|
||||
"unchanged surrounding text was sent again");
|
||||
setreply(&srv, 1, Rnormal, 0);
|
||||
pthread_mutex_lock(&srv.lock);
|
||||
srv.del = 2;
|
||||
pthread_mutex_unlock(&srv.lock);
|
||||
log.deln = 0;
|
||||
Check(key(ctx, win, GDK_KEY_j), "take-back key was not eaten");
|
||||
Check(log.deloffset == -2 && log.deln == 2,
|
||||
"the widget was told to take back %d runes at %d", log.deln,
|
||||
log.deloffset);
|
||||
pthread_mutex_lock(&srv.lock);
|
||||
srv.del = 0;
|
||||
pthread_mutex_unlock(&srv.lock);
|
||||
log.around[0] = '\0';
|
||||
|
||||
setpre(&srv, "reset");
|
||||
clearlog(&log);
|
||||
Check(key(ctx, win, GDK_KEY_e), "reset setup key was not eaten");
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -13,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;
|
||||
};
|
||||
@@ -81,8 +83,8 @@ cleanup:
|
||||
CT_CHECK(t, rmdir(root) == 0);
|
||||
}
|
||||
|
||||
static int
|
||||
ibusbegin(struct ct *t, Ibusfix *f)
|
||||
static void
|
||||
ibusbegin(Ibusfix *f)
|
||||
{
|
||||
Drawcmd dc;
|
||||
|
||||
@@ -97,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
|
||||
@@ -119,8 +120,6 @@ ibusend(Ibusfix *f)
|
||||
testengineinit(LangEN);
|
||||
while(channbrecv(drawc, &dc) > 0)
|
||||
;
|
||||
free(f->c1);
|
||||
free(f->c2);
|
||||
chanfree(replyc);
|
||||
replyc = f->oldreply;
|
||||
}
|
||||
@@ -168,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))
|
||||
@@ -213,8 +211,8 @@ ibus_private_input_policy(struct ct *t)
|
||||
{
|
||||
static const struct { u32int purpose; int want; } cases[] = {
|
||||
{ 0, 0 },
|
||||
{ Ibuspurposepassword, 1 },
|
||||
{ Ibuspurposepin, 1 },
|
||||
{ Purposepassword, 1 },
|
||||
{ Purposepin, 1 },
|
||||
{ 37, 0 },
|
||||
};
|
||||
Ibusfix f;
|
||||
@@ -223,8 +221,7 @@ ibus_private_input_policy(struct ct *t)
|
||||
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;
|
||||
@@ -239,7 +236,7 @@ ibus_private_input_policy(struct ct *t)
|
||||
contextkey(t, &f, ctx, 'n', Testctrlmask);
|
||||
res = contextkey(t, &f, ctx, 'k', 0);
|
||||
checkstr(t, "preedit", "k", &res.preedit);
|
||||
ctx->purpose = Ibuspurposepassword;
|
||||
ctx->purpose = Purposepassword;
|
||||
memset(&res, 0, sizeof res);
|
||||
CT_CHECK(t, !processkey(ctx, 'x', 0, text, sizeof text, &res));
|
||||
CT_CHECK(t, !res.eaten);
|
||||
@@ -274,8 +271,7 @@ ibus_context_lifecycle(struct ct *t)
|
||||
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))
|
||||
@@ -387,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;
|
||||
|
||||
@@ -23,6 +23,7 @@ typedef struct Response Response;
|
||||
struct Response
|
||||
{
|
||||
int eaten;
|
||||
int del;
|
||||
char commit[Ipcfieldmax+1];
|
||||
char preedit[Ipcfieldmax+1];
|
||||
};
|
||||
@@ -50,7 +51,8 @@ readresponseuntil(int fd, int want, Response *res, int64_t deadline,
|
||||
return rv;
|
||||
}
|
||||
res->eaten = hdr[0] != 0;
|
||||
n = getlen(hdr + 1);
|
||||
res->del = hdr[1];
|
||||
n = getlen(hdr + 2);
|
||||
if(n > Ipcfieldmax){
|
||||
fail("invalid commit length %zu", n);
|
||||
return -1;
|
||||
@@ -119,6 +121,18 @@ sendcap(int fd, int cap)
|
||||
return ipcsend(fd, req, sizeof req) == 0;
|
||||
}
|
||||
|
||||
static int
|
||||
sendsurround(int fd, char *text)
|
||||
{
|
||||
unsigned char req[Ipcreqsz];
|
||||
size_t n;
|
||||
|
||||
n = strlen(text);
|
||||
ipcpacksurround(req, n);
|
||||
return ipcsend(fd, req, sizeof req) == 0 &&
|
||||
(n == 0 || ipcsend(fd, text, n) == 0);
|
||||
}
|
||||
|
||||
static int
|
||||
expectresponse(int fd, int want, int eaten, char *commit, char *preedit,
|
||||
char *where)
|
||||
@@ -152,6 +166,33 @@ requestkey(int fd, int want, uint32_t mod, uint32_t key, int eaten,
|
||||
return expectresponse(fd, want, eaten, commit, preedit, where);
|
||||
}
|
||||
|
||||
/*
|
||||
* The Korean habit: 한자 typed, then the Hanja key. The 한 is the
|
||||
* client's by then, so the daemon must ask for it back and answer with
|
||||
* the whole word.
|
||||
*/
|
||||
static int
|
||||
requesthanja(int fd)
|
||||
{
|
||||
Response res;
|
||||
|
||||
if(!sendsurround(fd, "저는 한"))
|
||||
return fail("send surrounding text: %s", strerror(errno));
|
||||
if(!requestkey(fd, 1, Mctrl, 's', 1, "", "", "select Korean") ||
|
||||
!requestkey(fd, 1, 0, 'w', 1, "", "ㅈ", "jamo") ||
|
||||
!requestkey(fd, 1, 0, 'k', 1, "", "자", "syllable") ||
|
||||
!requestkey(fd, 1, Mctrl, 'h', 1, "", "자", "Hanja search"))
|
||||
return 0;
|
||||
if(!sendkey(fd, 1, 0, Kret))
|
||||
return fail("send Hanja pick: %s", strerror(errno));
|
||||
if(readresponseuntil(fd, 1, &res, nowms() + Calltimeout, 0) != 1)
|
||||
return 0;
|
||||
if(strcmp(res.commit, "漢字") != 0 || res.del != 1)
|
||||
return fail("Hanja pick committed %s and took back %d",
|
||||
res.commit, res.del);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int
|
||||
tryclient(char *path, int64_t deadline, int *client)
|
||||
{
|
||||
@@ -258,7 +299,8 @@ runsmoke(char *path)
|
||||
requestkey(client, 1, Mctrl, 'n', 1, "", "",
|
||||
"select Japanese") &&
|
||||
requestkey(client, 1, 0, 'k', 1, "", "k", "preedit") &&
|
||||
requestreset(client, 1, 1, "k", "", "reset");
|
||||
requestreset(client, 1, 1, "k", "", "reset") &&
|
||||
requesthanja(client);
|
||||
close(client);
|
||||
return ok;
|
||||
}
|
||||
|
||||
@@ -177,34 +177,37 @@ ipc_runtime_path(struct ct *t)
|
||||
void
|
||||
ipc_response_pack_boundaries(struct ct *t)
|
||||
{
|
||||
static const uchar withpre[] = { 1, 1, 0, 'A', 2, 0, 'x', 'y' };
|
||||
static const uchar withoutpre[] = { 1, 1, 0, 'A' };
|
||||
static const uchar withpre[] = { 1, 3, 1, 0, 'A', 2, 0, 'x', 'y' };
|
||||
static const uchar withoutpre[] = { 1, 0, 1, 0, 'A' };
|
||||
uchar out[Ipcmaxresp+1], field[Ipcfieldmax];
|
||||
int n;
|
||||
|
||||
n = ipcpackresp(out, sizeof out, 7, "A", 1, "xy", 2, 1);
|
||||
n = ipcpackresp(out, sizeof out, 7, 3, "A", 1, "xy", 2, 1);
|
||||
CT_EQ_INT(t, sizeof withpre, n);
|
||||
CT_EQ_MEM(t, withpre, out, sizeof withpre);
|
||||
n = ipcpackresp(out, sizeof out, 1, "A", 1, "xy", 2, 0);
|
||||
n = ipcpackresp(out, sizeof out, 1, 0, "A", 1, "xy", 2, 0);
|
||||
CT_EQ_INT(t, sizeof withoutpre, n);
|
||||
CT_EQ_MEM(t, withoutpre, out, sizeof withoutpre);
|
||||
n = ipcpackresp(out, sizeof out, 0, nil, 0, nil, 0, 1);
|
||||
n = ipcpackresp(out, sizeof out, 0, 0, nil, 0, nil, 0, 1);
|
||||
CT_EQ_INT(t, Ipcresphdrsz + Ipclensz, n);
|
||||
|
||||
memset(field, 'x', sizeof field);
|
||||
memset(out, 0xa5, sizeof out);
|
||||
n = ipcpackresp(out, Ipcmaxresp, 1,
|
||||
n = ipcpackresp(out, Ipcmaxresp, 1, 0,
|
||||
(char*)field, sizeof field, (char*)field, sizeof field, 1);
|
||||
CT_EQ_INT(t, Ipcmaxresp, n);
|
||||
CT_EQ_INT(t, 0, out[1]);
|
||||
CT_EQ_INT(t, 1, out[2]);
|
||||
CT_EQ_INT(t, 0, out[3+Ipcfieldmax]);
|
||||
CT_EQ_INT(t, 1, out[4+Ipcfieldmax]);
|
||||
CT_EQ_INT(t, 0, out[2]);
|
||||
CT_EQ_INT(t, 1, out[3]);
|
||||
CT_EQ_INT(t, 0, out[4+Ipcfieldmax]);
|
||||
CT_EQ_INT(t, 1, out[5+Ipcfieldmax]);
|
||||
CT_EQ_INT(t, 0xa5, out[Ipcmaxresp]);
|
||||
CT_EQ_INT(t, -1, ipcpackresp(out, Ipcmaxresp-1, 1,
|
||||
CT_EQ_INT(t, -1, ipcpackresp(out, Ipcmaxresp-1, 1, 0,
|
||||
(char*)field, sizeof field, (char*)field, sizeof field, 1));
|
||||
CT_EQ_INT(t, -1, ipcpackresp(out, sizeof out, 0,
|
||||
CT_EQ_INT(t, -1, ipcpackresp(out, sizeof out, 0, 0,
|
||||
(char*)field, Ipcfieldmax+1, "", 0, 0));
|
||||
/* A take-back is a rune count and never leaves its byte. */
|
||||
CT_EQ_INT(t, -1, ipcpackresp(out, sizeof out, 0, -1, "", 0, "", 0, 0));
|
||||
CT_EQ_INT(t, -1, ipcpackresp(out, sizeof out, 0, 256, "", 0, "", 0, 0));
|
||||
}
|
||||
|
||||
void
|
||||
@@ -215,8 +218,8 @@ ipc_response_empty_and_preedit(struct ct *t)
|
||||
Ipcresp resp;
|
||||
int fd[2], nfirst, nsecond;
|
||||
|
||||
nfirst = ipcpackresp(first, sizeof first, 0, "", 0, "", 0, 0);
|
||||
nsecond = ipcpackresp(second, sizeof second, 1,
|
||||
nfirst = ipcpackresp(first, sizeof first, 0, 0, "", 0, "", 0, 0);
|
||||
nsecond = ipcpackresp(second, sizeof second, 1, 0,
|
||||
"go", 2, "kana", 4, 1);
|
||||
if(!CT_CHECK(t, nfirst > 0 && nsecond > 0))
|
||||
return;
|
||||
@@ -252,9 +255,9 @@ ipc_response_max_and_drain(struct ct *t)
|
||||
int fd[2], nfirst, nsecond;
|
||||
|
||||
memset(field, 'x', sizeof field);
|
||||
nfirst = ipcpackresp(first, sizeof first, 1,
|
||||
nfirst = ipcpackresp(first, sizeof first, 1, 0,
|
||||
(char*)field, sizeof field, (char*)field, sizeof field, 1);
|
||||
nsecond = ipcpackresp(second, sizeof second, 0, "ok", 2, "", 0, 0);
|
||||
nsecond = ipcpackresp(second, sizeof second, 0, 0, "ok", 2, "", 0, 0);
|
||||
if(!CT_CHECK(t, nfirst == Ipcmaxresp && nsecond > 0))
|
||||
return;
|
||||
if(socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0){
|
||||
@@ -285,7 +288,7 @@ ipc_response_fragmented_and_truncated(struct ct *t)
|
||||
Ipcresp resp;
|
||||
int fd[2], i, n;
|
||||
|
||||
n = ipcpackresp(frame, sizeof frame, 1, "abc", 3, "xy", 2, 1);
|
||||
n = ipcpackresp(frame, sizeof frame, 1, 2, "abc", 3, "xy", 2, 1);
|
||||
if(!CT_CHECK(t, n > 0))
|
||||
return;
|
||||
if(socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0){
|
||||
@@ -299,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);
|
||||
}
|
||||
|
||||
@@ -767,7 +767,8 @@ ipcrequest(int fd, uint32_t mod, uint32_t key, unsigned char *want,
|
||||
int
|
||||
ipcprobe(int fd, char *where)
|
||||
{
|
||||
unsigned char empty[] = {0, 0, 0, 0, 0};
|
||||
/* eaten, take-back, no commit, no preedit */
|
||||
unsigned char empty[] = {0, 0, 0, 0, 0, 0};
|
||||
|
||||
return ipcrequest(fd, 0, 0, empty, sizeof empty, where);
|
||||
}
|
||||
|
||||
@@ -35,12 +35,11 @@ class MkemojiTest(unittest.TestCase):
|
||||
result = generate()
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
data = table(result.stdout)
|
||||
keys = list(data)
|
||||
self.assertEqual([k for k in keys if k[:1] == "^"][:9],
|
||||
[f"^{d}" for d in range(1, 10)])
|
||||
self.assertEqual([k for k in keys if k[:1] == "_"][:9],
|
||||
[f"_{d}" for d in range(1, 10)])
|
||||
self.assertEqual([k for k in keys if k[:1] == "<"][2], "<3")
|
||||
# The engine walks a prefix's children in rune order, so only the
|
||||
# prefix's own row decides which candidate a digit picks.
|
||||
self.assertEqual(data["^"].split(), list("¹²³⁴⁵⁶⁷⁸⁹"))
|
||||
self.assertEqual(data["_"].split(), list("₁₂₃₄₅₆₇₈₉"))
|
||||
self.assertEqual(data["<"].split()[2], "♥")
|
||||
self.assertEqual(data["^1"], "¹")
|
||||
self.assertEqual(data["_2"], "₂")
|
||||
self.assertEqual(data["<3"].split()[0], "♥")
|
||||
|
||||
@@ -64,6 +64,24 @@ class MkhanjaTest(unittest.TestCase):
|
||||
"\u91d1\t\uae40",
|
||||
"\u6f22\u5b57\t\ud55c\uc790"])
|
||||
|
||||
def test_imports_symbol_rows_under_a_jamo(self):
|
||||
result = run(
|
||||
IMPORT,
|
||||
self.source(
|
||||
"\u3141:\u203b:reference mark\n"
|
||||
"\u3134:\u300c:bracket\n"
|
||||
"\u3131:\u3000:ideographic space\n"
|
||||
"\u3131:\u00ad:soft hyphen\n"
|
||||
"\u3131:\u52a0:Hanja under a jamo\n"
|
||||
"\u3141:\u203b\u203b:two runes\n"
|
||||
"\u3141\u3134:\u203b:two jamo\n"
|
||||
"\u314f:\u203b:a vowel\n"
|
||||
),
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(result.stdout.splitlines(),
|
||||
["\u203b\t\u3141", "\u300c\t\u3134"])
|
||||
|
||||
def test_import_rejects_malformed_and_duplicate_rows(self):
|
||||
bad = [
|
||||
"한 漢\n",
|
||||
@@ -110,6 +128,19 @@ class MkhanjaTest(unittest.TestCase):
|
||||
],
|
||||
)
|
||||
|
||||
def test_groups_symbols_under_their_jamo(self):
|
||||
result = run(
|
||||
GENERATE,
|
||||
self.source(
|
||||
"※\tㅁ\n"
|
||||
"○\tㅁ\n"
|
||||
"「\tㄴ\n"
|
||||
),
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(result.stdout.splitlines(),
|
||||
["ㅁ\t※ ○", "ㄴ\t「"])
|
||||
|
||||
def test_generator_rejects_bad_rows(self):
|
||||
bad = [
|
||||
"漢a\t한\n",
|
||||
@@ -117,6 +148,12 @@ class MkhanjaTest(unittest.TestCase):
|
||||
"𠀀\t한\n",
|
||||
"漢\t한\n漢\t한\n",
|
||||
"漢 한\n",
|
||||
"加\tㅁ\n",
|
||||
"※\t한\n",
|
||||
"※\tㅁㄴ\n",
|
||||
"※※\tㅁ\n",
|
||||
"\u3000\tㄱ\n",
|
||||
"\u00ad\tㄱ\n",
|
||||
]
|
||||
for text in bad:
|
||||
with self.subTest(text=repr(text)):
|
||||
@@ -131,6 +168,15 @@ class MkhanjaTest(unittest.TestCase):
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn("한\t", result.stdout)
|
||||
|
||||
def test_generator_reads_both_sources(self):
|
||||
result = run(GENERATE)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
lines = result.stdout.splitlines()
|
||||
self.assertEqual(lines.count(";; All rights reserved."), 2)
|
||||
rows = dict(line.split("\t") for line in lines if "\t" in line)
|
||||
self.assertIn("漢", rows["한"].split())
|
||||
self.assertIn("※", rows["ㅁ"].split())
|
||||
|
||||
def test_generator_keeps_runtime_candidate_limit(self):
|
||||
rows = "".join(f"{chr(0x4E00 + n)}\t한\n" for n in range(129))
|
||||
result = run(GENERATE, self.source(rows))
|
||||
|
||||
@@ -86,6 +86,8 @@ void engine_emoji_preedit_languages(struct ct*);
|
||||
void engine_emoji_start_and_unknown(struct ct*);
|
||||
void engine_hanja_search(struct ct*);
|
||||
void engine_hanja_unknown_and_cancel(struct ct*);
|
||||
void engine_hanja_reaches_back(struct ct*);
|
||||
void engine_hanja_word_prefix(struct ct*);
|
||||
void engine_hanja_korean_keys(struct ct*);
|
||||
void engine_hanja_backspace(struct ct*);
|
||||
void engine_hanja_input_languages(struct ct*);
|
||||
@@ -125,3 +127,7 @@ void xim_adapter_callback_cleanup(struct ct*);
|
||||
void xim_adapter_callback_transfer(struct ct*);
|
||||
void xim_adapter_callback_owner_loss(struct ct*);
|
||||
void xim_adapter_commit_encoding(struct ct*);
|
||||
void wl_modifier_mask(struct ct*);
|
||||
void wl_forwarded_keys(struct ct*);
|
||||
void wl_surrounding_text(struct ct*);
|
||||
void wl_repeat_ends(struct ct*);
|
||||
|
||||
@@ -107,6 +107,8 @@ static const struct ct_test tests[] = {
|
||||
{ "engine/emoji-start-and-unknown", engine_emoji_start_and_unknown },
|
||||
{ "engine/hanja-search", engine_hanja_search },
|
||||
{ "engine/hanja-unknown-cancel", engine_hanja_unknown_and_cancel },
|
||||
{ "engine/hanja-reaches-back", engine_hanja_reaches_back },
|
||||
{ "engine/hanja-word-prefix", engine_hanja_word_prefix },
|
||||
{ "engine/hanja-korean-keys", engine_hanja_korean_keys },
|
||||
{ "engine/hanja-backspace", engine_hanja_backspace },
|
||||
{ "engine/hanja-input-languages", engine_hanja_input_languages },
|
||||
@@ -145,6 +147,10 @@ static const struct ct_test tests[] = {
|
||||
{ "xim/callback-transfer", xim_adapter_callback_transfer },
|
||||
{ "xim/callback-owner-loss", xim_adapter_callback_owner_loss },
|
||||
{ "xim/commit-encoding", xim_adapter_commit_encoding },
|
||||
{ "wl/modifier-mask", wl_modifier_mask },
|
||||
{ "wl/forwarded-keys", wl_forwarded_keys },
|
||||
{ "wl/surrounding-text", wl_surrounding_text },
|
||||
{ "wl/repeat-ends", wl_repeat_ends },
|
||||
};
|
||||
#else
|
||||
static const struct ct_test tests[] = {
|
||||
|
||||
165
tests/wl_adapter_test.c
Normal file
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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
@@ -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))
|
||||
@@ -736,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;
|
||||
@@ -891,8 +884,7 @@ 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;
|
||||
@@ -980,8 +972,7 @@ 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.clientpre = 1;
|
||||
@@ -996,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);
|
||||
}
|
||||
@@ -1016,8 +1006,7 @@ 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;
|
||||
@@ -1086,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);
|
||||
}
|
||||
|
||||
@@ -129,10 +129,10 @@ pumpinput(Display *dpy, XIC ic, char *commit, size_t cap, int timeout)
|
||||
continue;
|
||||
n = Xutf8LookupString(ic, &ev.xkey, buf, sizeof buf - 1,
|
||||
&sym, &status);
|
||||
if(n < 0 || ((status == XLookupChars || status == XLookupBoth) &&
|
||||
if(n < 0 || (status == XLookupChars &&
|
||||
((size_t)n >= cap || commit == NULL)))
|
||||
return 0;
|
||||
if((status == XLookupChars || status == XLookupBoth) && n != 0){
|
||||
if(status == XLookupChars && n != 0){
|
||||
memcpy(commit, buf, n);
|
||||
commit[n] = '\0';
|
||||
}
|
||||
@@ -273,7 +273,7 @@ sendkey(Display *dpy, XIC ic, Window win, KeySym sym, unsigned int state,
|
||||
if(n < 0)
|
||||
return fail("Xutf8LookupString failed for keysym %#lx",
|
||||
(unsigned long)sym);
|
||||
if((status == XLookupChars || status == XLookupBoth) && n != 0){
|
||||
if(status == XLookupChars && n != 0){
|
||||
if((size_t)n >= cap)
|
||||
return fail("commit buffer overflow");
|
||||
memcpy(commit, buf, n);
|
||||
@@ -405,6 +405,9 @@ testcallbacks(Display *dpy, XIM im, Window win)
|
||||
/*
|
||||
* The popup is the one viewable override-redirect window on the private
|
||||
* display; created before the client window, it must still stack above it.
|
||||
* The order and the map states have to be one observation: the daemon
|
||||
* raises and maps between two ungrabbed round trips, and a stale order read
|
||||
* against a fresh map state says the popup is below when it is not.
|
||||
*/
|
||||
static int
|
||||
popupabove(Display *dpy, Window client)
|
||||
@@ -419,8 +422,11 @@ popupabove(Display *dpy, Window client)
|
||||
for(;;){
|
||||
above = 0;
|
||||
root = DefaultRootWindow(dpy);
|
||||
if(!XQueryTree(dpy, root, &root, &parent, &kids, &n))
|
||||
XGrabServer(dpy);
|
||||
if(!XQueryTree(dpy, root, &root, &parent, &kids, &n)){
|
||||
XUngrabServer(dpy);
|
||||
return fail("query the window tree");
|
||||
}
|
||||
for(i = 0; i < n; i++){
|
||||
if(kids[i] == client)
|
||||
above = 1;
|
||||
@@ -429,6 +435,7 @@ popupabove(Display *dpy, Window client)
|
||||
break;
|
||||
}
|
||||
XFree(kids);
|
||||
XUngrabServer(dpy);
|
||||
if(i < n)
|
||||
return above || fail("popup is stacked below the client");
|
||||
if(leftms(deadline) == 0)
|
||||
|
||||
4
win.c
4
win.c
@@ -197,11 +197,7 @@ wininit(void)
|
||||
xcb_randr_query_version_cookie_t rc;
|
||||
xcb_randr_query_version_reply_t *rr;
|
||||
const xcb_query_extension_reply_t *rext;
|
||||
char *scale;
|
||||
|
||||
scale = getenv("GDK_SCALE");
|
||||
if(scale != nil && atoi(scale) > 1)
|
||||
popupscale = min(atoi(scale), 4);
|
||||
conn = xcb_connect(nil, &n);
|
||||
if(xcb_connection_has_error(conn)){
|
||||
fprint(2, "strans: popup disabled: cannot connect to X display\n");
|
||||
|
||||
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");
|
||||
}
|
||||
3
xim.c
3
xim.c
@@ -384,7 +384,7 @@ kpress(Ic *state, xcb_key_press_event_t *ev)
|
||||
int n;
|
||||
|
||||
sym = keymaplookup(kstate, ev->detail, ev->state);
|
||||
if(composekey(sym, text, sizeof text))
|
||||
if(composekey(Composexim, state, sym, text, sizeof text))
|
||||
return;
|
||||
key = ipckeysym(sym, xkb_keysym_to_utf32(sym));
|
||||
if(keymeaningful(key))
|
||||
@@ -424,6 +424,7 @@ icfree(void *p)
|
||||
state = p;
|
||||
/* The owner remains valid until imthread acknowledges its release. */
|
||||
release(state);
|
||||
composedrop(Composexim, state);
|
||||
icunlink(state);
|
||||
free(state);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user