194 lines
8.6 KiB
Markdown
194 lines
8.6 KiB
Markdown
# strans
|
||
|
||
strans is a small, single-user input method for Korean, Japanese, English,
|
||
emoji and symbols, with Vietnamese Telex as a compatibility mode. One
|
||
engine serves four frontends: `zwp_input_method_v2` on Wayland — sway and
|
||
any other compositor that offers it — IBus, which GTK 4 and Qt use, XIM,
|
||
and a GTK 3 module.
|
||
|
||
## Modes
|
||
|
||
| Key | Mode |
|
||
| --- | --- |
|
||
| `Ctrl+S` | Korean Hangul (2-beolsik) |
|
||
| `Ctrl+N` | Japanese Hiragana, converting to Kanji |
|
||
| `Ctrl+K` | Japanese Katakana |
|
||
| `Ctrl+T` | English |
|
||
| `Ctrl+V` | Vietnamese Telex |
|
||
| `Ctrl+E` | Emoji and symbol search |
|
||
| `Ctrl+H` | One-shot Hanja and symbol search |
|
||
|
||
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.
|
||
|
||
## Composing
|
||
|
||
Japanese composes a whole reading and then offers its Kanji with none
|
||
chosen. `Space` and `Tab` step through the candidates and wrap (`Shift`
|
||
reverses), the reading in Katakana last, so a word the dictionary lacks
|
||
converts with one `Space`; `Up`/`Down` and `PageUp`/`PageDown` move without
|
||
wrapping. `Enter` commits the chosen candidate, or the reading, as `0` and
|
||
typing on always do. Once a candidate is chosen `1`-`9` take that row of
|
||
the page shown; before that the digits type. `Backspace` deletes the last
|
||
kana shown, and romaji that made no kana stays as typed until it is mended;
|
||
`Esc` cancels the reading, and on a chosen candidate both go back to it.
|
||
The dictionary keys verbs and adjectives by stem and okurigana, so `kaku`
|
||
offers かく's readings and then 書く and its kin — typing the okurigana
|
||
with `Shift`, as SKK does, `kaKu`, asks for that split first. Katakana
|
||
mode does not convert, so there `Space` and `Tab` commit the reading and
|
||
go on to the application.
|
||
|
||
Korean composes one syllable at a time. `Enter`, `Tab`, `Esc` and any key
|
||
that is not a jamo commit the syllable and go on to the application, so
|
||
`Esc` still leaves insert mode. Two lone consonants join into the compound
|
||
final they make (rt → ㄳ), and a vowel typed before its consonant is
|
||
reordered under it (kr → 가), as in libhangul.
|
||
|
||
A search takes the keys until `Enter`, `Space` or `1`-`9` picks a result or
|
||
`Esc` cancels, then returns to the previous mode. Emoji matches the typed
|
||
keys, and what they spell in the current mode, against a prefix of every
|
||
emoji's CLDR name and keywords in English, Korean and Japanese, and against
|
||
ASCII aliases such as `->` and `<=`. `Ctrl+H` takes the syllable being
|
||
composed as its query and composes on from it, converting a word as well as
|
||
a syllable — 한자 gives 漢字, 대한민국 gives 大韓民國 — and `Esc` gives the
|
||
syllable back. A word is committed a syllable at a time, so the reading
|
||
also reaches back into the text the application already holds, as far as
|
||
the dictionary knows the whole of it: type 한자 and then `Ctrl+H` and the
|
||
query is 한자, not 자, and picking 漢字 takes the 한 back. `Backspace`
|
||
gives that reach back before it deletes what was typed, so 상 written
|
||
with 태 typed answers 狀態, and one `Backspace` answers 態 for the
|
||
syllable alone. A reading is also the start of the longer words it
|
||
begins, whose conversions follow its own, so 대한 answers 大寒 first and
|
||
大韓民國 further down. A lone consonant is a reading too, and answers
|
||
with the symbol table a Korean keyboard's 한자 key has always offered:
|
||
ㅁ gives ※ ○ △ ㈜, ㄴ the brackets 「」『』, ㄹ the units ℃ ㎏ ℓ, ㅇ the
|
||
circled numbers ①②③.
|
||
|
||
Reaching back needs the application to hand over the text around its
|
||
cursor and to take some of it away again. The Wayland input method, IBus
|
||
and the GTK 3 module all can; XIM has no such request, so there the
|
||
reading is what is still being composed and nothing else, and an IBus
|
||
client that reads a key's effects only after the call returns is not
|
||
asked for its text either, since a deletion could not be ordered before
|
||
the commit.
|
||
|
||
Dead keys and Compose sequences are composed by strans itself for the
|
||
Wayland, XIM and IBus frontends, from `XCOMPOSEFILE` or the locale; the
|
||
GTK 3 module leaves them to GtkIMContextSimple.
|
||
|
||
## Preedit and candidates
|
||
|
||
| Frontend | Preedit | Candidates | Reaches back |
|
||
| --- | --- | --- | --- |
|
||
| Wayland input method | inline in the client | popup | yes |
|
||
| GTK 3 and IBus | inline in the client | popup | yes |
|
||
| XIM PreeditCallbacks | inline through XIM callbacks | popup | no |
|
||
| XIM PreeditPosition, PreeditNothing | popup | popup | no |
|
||
|
||
On Wayland the popup is a surface the compositor places at the text cursor,
|
||
flipping it above the line when there is no room below. Elsewhere it is an
|
||
X11 window, placed from the XIM spot or from the GTK caret, and XIM text
|
||
travels as `COMPOUND_TEXT`. Either way `GDK_SCALE` sizes it for HiDPI
|
||
displays.
|
||
|
||
## Build and test
|
||
|
||
```sh
|
||
git submodule update --init
|
||
make docker-image
|
||
make docker-build
|
||
```
|
||
|
||
That leaves `strans`, the daemon, and `gtk/im-strans.so`, the GTK 3 module.
|
||
Tests come in three tiers:
|
||
|
||
```sh
|
||
make check # generated-map validation and unit tests
|
||
make check-live # one IBus, GTK, XIM and IPC daemon smoke each
|
||
make check-stress # randomized, capacity, collision, failure and restart
|
||
```
|
||
|
||
`make docker-check` runs all three in the container, and
|
||
`make docker-valgrind` the unit suite under Valgrind; rerun
|
||
`make docker-image` after changing `Dockerfile`. `UNITARGS=hangul` filters
|
||
the unit suite alone, not the other tiers.
|
||
|
||
A native build wants a C toolchain, Make, pkg-config and Plan 9 port, plus
|
||
development files for D-Bus, XCB and xcb-imdkit, Wayland and
|
||
`wayland-scanner`, xkbcommon, Pango and Cairo, and GTK 3; the tests also
|
||
want Xvfb, Python 3 and fonts covering Latin, CJK and emoji.
|
||
[`Dockerfile`](Dockerfile) names the exact packages.
|
||
|
||
## Run
|
||
|
||
```sh
|
||
./run.sh # restart the daemon in the background
|
||
./strans map # or run it in the foreground
|
||
```
|
||
|
||
`./strans DIR` reads `hira.map`, `kata.map`, `telex.map`, `kanji.dict`,
|
||
`emoji.dict` and `hanja.dict` from `DIR` at runtime; nothing but the GTK
|
||
module is installed. A service supervisor wants the session's
|
||
`XDG_RUNTIME_DIR` and its `WAYLAND_DISPLAY` or `DISPLAY`: the GTK module
|
||
finds the daemon at `$XDG_RUNTIME_DIR/strans.sock`, else
|
||
`/tmp/strans.UID`, and IBus clients through the address file libibus
|
||
expects under `~/.config/ibus/bus/`, which strans writes as `ibus-daemon`
|
||
would.
|
||
|
||
strans shows one popup per session, so it picks a frontend at startup: a
|
||
compositor offering `zwp_input_method_v2` gets the Wayland frontend, and
|
||
then neither XIM nor the X11 popup runs; every other session gets XIM. The
|
||
IBus endpoint and the GTK 3 module are served in both.
|
||
|
||
On Wayland, then, set none of the variables below. GTK, Qt and Firefox
|
||
speak text-input-v3 themselves — GTK 4 binds it with `GTK_IM_MODULE` unset
|
||
— and setting these is what pushes them off the path that works: such a
|
||
client still composes inline but shows no candidates, because the popup
|
||
belongs to the Wayland frontend. Chromium is beyond help either way, since
|
||
it asks for `text-input-v1`, which wlroots does not implement.
|
||
|
||
```sh
|
||
export XMODIFIERS=@im=strans # XIM
|
||
doas make -C gtk install # GTK 3 module
|
||
export GTK_IM_MODULE=strans
|
||
GLFW_IM_MODULE=ibus kitty # IBus client example
|
||
```
|
||
|
||
strans provides its own IBus endpoint, so `ibus-daemon` and fcitx are not
|
||
needed. Building the GTK module wants GTK development files; installing
|
||
one built elsewhere does not. The install target takes `GTK_MODULE_DIR`
|
||
when set, else asks GTK where its modules live; `doas make -C gtk uninstall`
|
||
removes it. With neither `WAYLAND_DISPLAY` nor `DISPLAY` the daemon still
|
||
serves IBus and its socket, but nothing draws a popup.
|
||
|
||
## Dictionary data
|
||
|
||
After changing an input source, regenerate and verify:
|
||
|
||
```sh
|
||
python3 map/mktelex.py >map/telex.map
|
||
map/mkemoji >map/emoji.dict
|
||
map/mkhanja >map/hanja.dict
|
||
make verify-map
|
||
```
|
||
|
||
[`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, warms it up and records the workload with Linux `perf` into
|
||
`bench/perf.data`.
|
||
|
||
## Licensing
|
||
|
||
Third-party notices are in [`LICENSES`](LICENSES). The repository declares
|
||
no license for the strans source as a whole.
|