Compare commits

...

2 Commits

Author SHA1 Message Date
279b9230a4 docs: simplify README 2026-08-14 13:43:45 +09:00
c2cb18cbdd run: return after background startup 2026-08-14 13:39:07 +09:00
2 changed files with 82 additions and 468 deletions

241
README.md
View File

@@ -1,63 +1,34 @@
# strans
strans is a small, single-user input method for Korean, Japanese, English,
and emoji. It provides one engine shared by IBus, XIM, and GTK 3 frontends.
The existing Vietnamese Telex mode remains available for compatibility, but
is not an actively developed language mode.
emoji, and symbols. It provides one engine for IBus, XIM, and GTK 3.
Vietnamese Telex is also available as a compatibility mode.
## Input behavior
## Input modes
English is direct passthrough. Korean uses 2-beolsik composition. Japanese
Hiragana keeps the complete kana reading until it is committed, so `kanji`
forms `かんじ` and offers whole-reading candidates such as `漢字` and `幹事`.
An apostrophe resolves an ambiguous `n` without forwarding the apostrophe:
for example, `n'ya` forms `んや`. Katakana is direct Katakana composition
and does not perform Kanji dictionary conversion.
| Key | Mode |
| --- | --- |
| `Ctrl+N` | Japanese Hiragana and Kanji conversion |
| `Ctrl+K` | Japanese Katakana |
| `Ctrl+S` | Korean Hangul (2-beolsik) |
| `Ctrl+T` | English |
| `Ctrl+V` | Vietnamese Telex |
| `Ctrl+E` | Emoji and symbol search |
| `Ctrl+H` | One-shot Hanja search |
| `Ctrl+P` | Toggle popup preedit |
A Japanese composition is committed by Enter, candidate selection, a real
input boundary, or an explicit mode change. Backspace first removes pending
romaji, then accumulated kana. The Japanese modes support doubled consonants
and combinations such as `nya`, `nnya`, and `matcha`.
Use `Up`/`Down` to move through candidates, `Enter` or `1`-`9` to select,
`Tab` to cycle temporary search results, and `Esc` to cancel. `0` commits the
current reading without conversion.
Modes and temporary searches use Ctrl and one letter:
```text
Ctrl+N Japanese Hiragana and Kanji conversion
Ctrl+K Japanese Katakana
Ctrl+S Korean Hangul
Ctrl+T English passthrough
Ctrl+V Vietnamese Telex compatibility mode
Ctrl+E Emoji and symbol search
Ctrl+H One-shot Hanja search
Ctrl+P Toggle popup preedit
```
While candidates are visible, Up and Down move the selection, Enter accepts
the selected candidate, and an unmodified `1`-`9` selects the corresponding
visible row. Tab cycles temporary search results. `0` commits an unconverted
language reading. Escape cancels the current composition. Ctrl, Alt, or
Super combined with a number does not select a candidate.
Emoji search is temporary and returns to the previous mode. Queries may use
English aliases or the active language's input rules, including Japanese
`egao` becoming `えがお` or `エガオ`:
```text
Ctrl+E → smile → Enter → 😀
```
Hanja search is temporary and returns to the active language after one
conversion. Press Ctrl+H and type with that language's normal input rules,
then select with Up, Down, Tab, Enter, or `1`-`9`. The bundled dictionary
currently indexes exactly one modern Hangul syllable. Incomplete jamo,
multiple syllables, and other languages therefore have no candidates; Enter
commits the displayed reading unchanged. Escape cancels the search. It does
not convert words or text already surrounding the cursor.
Hiragana mode composes a complete reading before offering Kanji candidates.
Katakana mode does not perform Kanji conversion. Emoji and Hanja searches
return to the previous mode after use. Hanja lookup currently supports one
modern Hangul syllable at a time.
## Build and test
The supported build uses Docker so host and container toolchains are not
mixed. Only Docker and Make are needed on the host:
Docker is the supported build environment:
```sh
git submodule update --init
@@ -66,12 +37,7 @@ make docker-build
make docker-check
```
`docker-image` creates the Arch Linux dependency image, `docker-build`
compiles the runtime components, and `docker-check` runs the map verifiers and
test suites. Docker targets force their own compilation, so newer host
objects cannot be reused. `docker` is a convenience alias for the image and
build steps.
The resulting runtime artifacts are:
Build output:
```text
strans daemon and IBus frontend
@@ -79,12 +45,44 @@ xim/strans-xim XIM frontend
gtk/im-strans.so GTK 3 frontend
```
Use `make docker-check TESTARGS=hangul` to filter the C unit tests. A native
build is still possible with the dependencies and Plan 9 setup recorded in
`Dockerfile`; use `make all`, `make check`, and `make bench` inside that
environment.
Use `make docker-check TESTARGS=hangul` to run selected C tests. Native
`make all`, `make check`, and `make bench` are also available when the
dependencies listed in [`Dockerfile`](Dockerfile) are installed.
After editing an input source, regenerate and verify its dictionary with:
## Run
The popup needs one to four scalable font files. No fonts are bundled.
`run.sh` uses installed DejaVu, Jigmo, or Noto CJK fonts when available, or
accepts explicit files in fallback order:
```sh
./run.sh
./run.sh /path/to/primary.ttf /path/to/fallback.ttc
```
The script starts the daemon and, when `DISPLAY` is set, the XIM frontend. It
restarts any existing `strans` processes owned by the current user.
Configure clients as needed:
```sh
# XIM
export XMODIFIERS=@im=strans
# GTK 3 module
doas make -C gtk install
# IBus client example
GLFW_IM_MODULE=ibus kitty
```
strans provides its own IBus endpoint; `ibus-daemon` and fcitx are not
required. Without `DISPLAY`, the daemon and IBus frontend still work, but the
popup is disabled.
## Dictionary data
After changing an input source, regenerate and verify the dictionaries:
```sh
map/mkemoji >map/emoji.dict
@@ -92,119 +90,12 @@ map/mkhanja >map/hanja.dict
make verify-map
```
Hanja import and Japanese dictionary regeneration are documented in
[`map/README`](map/README). The converter output is deterministic UTF-8,
tab-separated data consumable by the runtime dictionary reader.
See [`map/README`](map/README) for Hanja import and Japanese dictionary
generation.
## Run and install
## Licensing
Install one or more scalable outline fonts, then pass their files in fallback
order after the map directory. The daemon accepts at most four font files and
does not search the system font database. For example, the Docker-tested Arch
packages and paths are:
```sh
doas pacman -S ttf-dejavu ttf-jigmo
./strans map \
/usr/share/fonts/TTF/DejaVuSans.ttf \
/usr/share/fonts/TTF/Jigmo.ttf \
/usr/share/fonts/TTF/Jigmo2.ttf &
```
These fonts are test fixtures, not runtime requirements. Any suitable
scalable outline files may be supplied; their argument order is the glyph
fallback order. A collection file uses its first face. If any named file is
absent or invalid, the popup disables itself cleanly while input processing
continues. `run.sh` and `bench.sh` accept the same font-file arguments.
When `run.sh` is invoked without arguments, it uses the installed DejaVu,
Jigmo, or Noto CJK font files from the standard Arch Linux paths. Explicit
font-file arguments replace these defaults:
```sh
./run.sh
./run.sh /path/to/primary.ttf /path/to/fallback.ttc
```
`run.sh` remains in the foreground and owns only the daemon and XIM process
that it starts. It waits for that daemon's IPC socket and IBus address file
before reporting success. With `DISPLAY` set it then starts XIM; without a
display it keeps the headless daemon in the foreground and does not start
XIM. A signal or XIM exit stops and reaps the launcher's daemon. On repeated
invocation, it stops a previous live `run.sh` only after confirming that the
old launcher directly owns the daemon serving the same IPC and IBus
endpoints and that the daemon carries that launcher's PID and process-start
identity. A manually started or orphaned daemon is left untouched and
reported as a visible startup failure.
The IPC socket is placed at `$XDG_RUNTIME_DIR/strans.sock` when
`XDG_RUNTIME_DIR` is an absolute path. Otherwise strans uses the per-user
fallback `/tmp/strans.<uid>`. A second daemon will not replace a live
daemon's socket.
For XIM applications:
```sh
./xim/strans-xim &
export XMODIFIERS=@im=strans
xterm
```
For GTK 3 applications, install the module built by Docker. Installation
needs the GTK 3 runtime, but not its development package:
```sh
doas make -C gtk install
```
`LIBDIR` defaults to `/usr/lib` and may be overridden for another system.
Normal `DESTDIR` semantics are supported, and a staged install does not update
the host GTK module cache:
```sh
make -C gtk install DESTDIR="$pkgdir"
```
For IBus clients such as kitty:
```sh
GLFW_IM_MODULE=ibus kitty
```
strans provides its own IBus endpoint; neither ibus-daemon nor fcitx is
required. Starting without `DISPLAY` is supported: input processing and IBus
continue to run while the optional X popup is disabled.
## Ownership and rendering constraints
There is one global composition, not one independent engine per frontend
context. The first meaningful key from a new context resets the previous
composition and transfers ownership. Reset, focus loss, context destruction,
and disconnect affect the engine only when they come from its active owner;
stale events cannot clear a newer owner's preedit.
The popup is an optional X window. It uses the explicit font-file order and
fixed-width rune cells; U+FE0E and U+FE0F variation selectors use zero cells.
It does not perform OpenType shaping, color-emoji rendering, or general
grapheme clustering. Multi-rune emoji are committed intact but are drawn as
their constituent rune cells under this boundary. A supplied caret rectangle
is used when available, with the pointer as fallback. The X renderer accepts
only a 24-depth, 32-bpp TrueColor root format and disables itself cleanly on
other visuals.
## Data, third-party code, and licensing
Data and third-party provenance is recorded in
[`docs/PROVENANCE.md`](docs/PROVENANCE.md), with font-specific details in
[`font/PROVENANCE`](font/PROVENANCE). No font binaries are bundled; that file
retains the provenance of the faces formerly kept in the repository. The
historical SKK-derived Kanji
dictionary is distributed under GPL version 2 or later; its license text is
in [`LICENSES/GPL-2.0-or-later.txt`](LICENSES/GPL-2.0-or-later.txt).
The single-character Hanja data derived from libhangul is distributed under
the BSD 3-Clause license in
[`LICENSES/BSD-3-Clause-libhangul-hanja.txt`](LICENSES/BSD-3-Clause-libhangul-hanja.txt).
The repository does not currently declare a license for the strans project
source as a whole. The licenses of bundled data, historical font assets, and
third-party components do not by themselves license the original strans
source.
Data and third-party notices are in
[`docs/PROVENANCE.md`](docs/PROVENANCE.md) and
[`font/PROVENANCE`](font/PROVENANCE). The repository does not currently
declare a license for the strans source as a whole.

309
run.sh
View File

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