Compare commits

...

37 Commits

Author SHA1 Message Date
e69883d0ad build: restore the single-instance launcher 2026-08-12 18:32:18 +09:00
9051c69b49 build: install GTK without development files 2026-08-12 18:01:39 +09:00
9566995c07 fix: separate Bash import streams 2026-08-12 17:42:25 +09:00
1f9af6ee3e fix: reject unsupported SKK expressions 2026-08-12 17:39:09 +09:00
bae39c0188 build: force Docker-owned compilation 2026-08-12 17:34:48 +09:00
4e90fdd5c7 fix: harden caret and IBus dispatch 2026-08-12 17:34:34 +09:00
de536da0bb refactor: trim popup and XIM helpers 2026-08-12 17:33:14 +09:00
8d68e00f2b data: simplify and validate Japanese imports 2026-08-12 17:32:28 +09:00
a8a6c3b455 fix: construct Plan 9 paths without truncation 2026-08-12 17:28:17 +09:00
0d06ba43cd refactor: remove conditional compilation layers 2026-08-12 17:27:42 +09:00
91931196e2 refactor: simplify the Plan 9 runtime loop 2026-08-12 17:26:55 +09:00
211a6917b2 refactor: remove guarded helper headers 2026-08-12 16:45:27 +09:00
4e8720a40d fix: terminate XIM UTF-8 text runs 2026-08-12 16:43:57 +09:00
71af269efe fix: preserve emoji commits through XIM 2026-08-12 16:40:58 +09:00
3f5e107230 docs: record data provenance and runtime constraints 2026-08-12 16:24:22 +09:00
c41a1b4227 build: remove generated artifacts and unsafe scripts 2026-08-12 16:23:45 +09:00
368e6adcd4 fix: keep the headless daemon running 2026-08-12 16:22:58 +09:00
5927f6a5e1 fix: forward input at the composition limit 2026-08-12 16:21:33 +09:00
9d60c4bbb0 fix: release XIM ownership on reset 2026-08-12 16:20:47 +09:00
c7d4f16d3e test: stress engine transitions 2026-08-12 16:16:25 +09:00
a03efcf6bf test: cover IPC response framing 2026-08-12 16:11:22 +09:00
432df3730c fix: harden the per-user IPC endpoint 2026-08-12 16:08:23 +09:00
4120f90736 fix: make popup rendering deterministic 2026-08-12 15:59:52 +09:00
9c43bcb75c fix: repair Wayland key lifecycle 2026-08-12 15:58:24 +09:00
1d8d711882 build: install the GTK module predictably 2026-08-12 15:57:18 +09:00
1295480858 fix: repair XIM context lifecycle 2026-08-12 15:56:43 +09:00
103f2ec448 fix: harden small core containers 2026-08-12 15:54:16 +09:00
cec62cc40f fix: repair IBus context lifecycle 2026-08-12 15:49:28 +09:00
bc5712b6ac fix: retain the active owner's caret 2026-08-12 15:44:04 +09:00
5ba5cf209c fix: assign one active engine owner 2026-08-12 15:41:46 +09:00
9c244d1dd1 fix: retain the newest dictionary request 2026-08-12 15:37:50 +09:00
fabce14933 fix: tighten emoji lookup contracts 2026-08-12 15:36:37 +09:00
d0def814ef refactor: remove empty language maps 2026-08-12 15:35:02 +09:00
d2fa51ceba data: validate and repair Japanese dictionaries 2026-08-12 15:34:00 +09:00
f0ba7fc5f6 fix: normalize Korean shifted input 2026-08-12 15:32:28 +09:00
6dfbc08704 fix: correct Katakana composition 2026-08-12 15:29:35 +09:00
ec7e6eb992 fix: preserve complete Japanese readings 2026-08-12 15:28:16 +09:00
72 changed files with 4252 additions and 112111 deletions

65
.gitignore vendored
View File

@@ -1,59 +1,10 @@
strans /*.o
strans-xim /strans
/tests/*.o
/tests/unit_test /tests/unit_test
/xim/*.o
/xim/strans-xim
/xim/xim_test
/gtk/im-strans.so
/bench/bench /bench/bench
/bench/perf.data*
# Prerequisites
*.d
# Object files
*.o
*.ko
*.obj
*.elf
# Linker output
*.ilk
*.exp
# Precompiled Headers
*.gch
*.pch
# Libraries
*.lib
*.a
*.la
*.lo
# Shared objects (inc. Windows DLLs)
*.dll
*.so
*.so.*
*.dylib
# Executables
*.exe
*.out
*.app
*.i*86
*.x86_64
*.hex
# Debug files
*.dSYM/
*.su
*.idb
*.pdb
# Kernel Module Compile Results
*.mod*
*.cmd
.tmp_versions/
modules.order
Module.symvers
Mkfile.old
dkms.conf
# debug information files
*.dwo

View File

@@ -1,4 +1,4 @@
FROM archlinux:base FROM archlinux:base@sha256:b0deabeb3d283da2c7f7dbf0eea051b7b2cd0554e0b737cc457fd21683bdcdd1
RUN pacman -Syu --noconfirm --needed \ RUN pacman -Syu --noconfirm --needed \
diffutils \ diffutils \

View File

@@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

11
LICENSES/README.md Normal file
View File

@@ -0,0 +1,11 @@
# License inventory
- `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.
- Font license texts are stored in `font/` and associated with exact files
by `font/PROVENANCE`.
- `stb_truetype.h` includes its complete dual MIT/Public Domain terms.
- Generated Wayland files include their MIT-style notices in each file.
No repository-wide license has been declared for original strans source.

View File

@@ -5,6 +5,8 @@ PKG_LIBS = $(shell pkg-config --libs dbus-1 wayland-client xkbcommon)
CFLAGS = -Wall -Wextra -O2 -g $(PKG_CFLAGS) CFLAGS = -Wall -Wextra -O2 -g $(PKG_CFLAGS)
PROG = strans PROG = strans
DOCKER_IMAGE = strans-build DOCKER_IMAGE = strans-build
DOCKER_RUN = docker run --rm --user "$$(id -u):$$(id -g)" \
-v "$(CURDIR):/src" $(DOCKER_IMAGE)
SRCS = $(wildcard *.c) SRCS = $(wildcard *.c)
OBJS = $(SRCS:.c=.o) OBJS = $(SRCS:.c=.o)
@@ -12,7 +14,7 @@ OBJS = $(SRCS:.c=.o)
all: $(PROG) xim gtk all: $(PROG) xim gtk
$(PROG): $(OBJS) $(PROG): $(OBJS)
$(LD) -o $@ $(OBJS) -lthread -lString -lbio -lxcb -lm $(PKG_LIBS) $(LD) -o $@ $(OBJS) -lthread -lbio -lxcb -lm $(PKG_LIBS)
$(OBJS): dat.h fn.h ipc.h $(OBJS): dat.h fn.h ipc.h
# Avoid Arch's GLIBC_2.43 sqrtf optimization for older glibc runtimes. # Avoid Arch's GLIBC_2.43 sqrtf optimization for older glibc runtimes.
@@ -37,18 +39,23 @@ gtk:
bench: bench:
$(MAKE) -C bench/ $(MAKE) -C bench/
docker: Dockerfile docker-image: Dockerfile
docker build -t $(DOCKER_IMAGE) - < Dockerfile docker build -t $(DOCKER_IMAGE) - < Dockerfile
docker run --rm --user "$$(id -u):$$(id -g)" \
-v "$(CURDIR):/src" $(DOCKER_IMAGE) \
sh -c 'make clean && make all'
docker-check: docker docker-build:
docker run --rm --user "$$(id -u):$$(id -g)" \ $(DOCKER_RUN) make -B all
-e TESTARGS="$(TESTARGS)" -v "$(CURDIR):/src" \
$(DOCKER_IMAGE) make check docker-check:
$(DOCKER_RUN) make -B check TESTARGS="$(TESTARGS)"
docker-bench:
$(DOCKER_RUN) make -B bench
docker: docker-image
$(MAKE) docker-build
check: verify-map test check: verify-map test
$(MAKE) -C xim check
test: test:
$(MAKE) -C tests check TESTARGS="$(TESTARGS)" $(MAKE) -C tests check TESTARGS="$(TESTARGS)"
@@ -56,6 +63,9 @@ test:
verify-map: verify-map:
python3 map/mktelex.py | cmp - map/telex.map python3 map/mktelex.py | cmp - map/telex.map
python3 -B map/mkemoji | cmp - map/emoji.dict python3 -B map/mkemoji | cmp - map/emoji.dict
python3 -B map/verifymap.py map/*.map map/*.dict
python3 -B tests/mkemoji_test.py python3 -B tests/mkemoji_test.py
python3 -B tests/skk2ktrans_test.py
.PHONY: all check test verify-map clean xim gtk bench docker docker-check .PHONY: all check test verify-map clean xim gtk bench docker docker-image \
docker-build docker-check docker-bench

188
README.md
View File

@@ -1,47 +1,105 @@
# strans # strans
A small input method daemon for Japanese, Korean, Vietnamese, emoji, and strans is a small, single-user input method for Korean, Japanese, English,
symbols on X11 and Wayland. Inspired by 9front's ktrans. and emoji on X11 and Wayland. It provides one engine shared by XIM, GTK 3,
IBus, and Wayland input-method-v2 frontends. The existing Vietnamese Telex
mode remains available for compatibility, but is not an actively developed
language mode.
## Build ## Input behavior
The recommended build needs only Docker and Make on the host: 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.
```sh A Japanese composition is committed by Enter, candidate selection, a real
make docker 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`.
This creates an Arch Linux build image, compiles all runtime components in one Modes are selected with Ctrl and one letter:
disposable container, and writes three artifacts into the source tree:
```text ```text
strans daemon, IBus and Wayland frontend 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+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 emoji 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 → 😀
```
## 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:
```sh
git submodule update --init
make docker-image
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:
```text
strans daemon, IBus and Wayland frontends
xim/strans-xim XIM frontend xim/strans-xim XIM frontend
gtk/im-strans.so GTK 3 frontend gtk/im-strans.so GTK 3 frontend
``` ```
The build starts clean so host and container object files are never mixed. It Use `make docker-check TESTARGS=hangul` to filter the C unit tests. A native
removes existing build and test outputs; the `strans-build` dependency image is build is still possible with the dependencies and Plan 9 setup recorded in
kept as a cache. No development packages are required on the host. `Dockerfile`; use `make all`, `make check`, and `make bench` inside that
environment.
Void Linux runtime dependencies: After editing `map/emoji.src`, regenerate and verify its dictionary with:
```sh ```sh
sudo xbps-install -S dbus-libs wayland libxkbcommon libxcb xcb-imdkit gtk+3 map/mkemoji >map/emoji.dict
make verify-map
``` ```
For a native Arch Linux build, use `Dockerfile` as the dependency and Plan 9 Japanese dictionary regeneration and verification are documented in
setup checklist, then run `make`. Build the benchmark separately with [`map/README`](map/README). The converter output is deterministic UTF-8,
`make bench`. tab-separated data consumable by the runtime dictionary reader.
## Run ## Run and install
Start the daemon: Start the daemon with the installed map and font directories:
```sh ```sh
./strans map font & ./strans map font &
``` ```
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: For XIM applications:
```sh ```sh
@@ -50,11 +108,19 @@ export XMODIFIERS=@im=strans
xterm xterm
``` ```
For GTK 3 applications: For GTK 3 applications, install the module built by Docker. Installation
needs the GTK 3 runtime, but not its development package:
```sh ```sh
sudo make -C gtk install doas make -C gtk install
GTK_IM_MODULE=strans gedit ```
`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: For IBus clients such as kitty:
@@ -63,56 +129,38 @@ For IBus clients such as kitty:
GLFW_IM_MODULE=ibus kitty GLFW_IM_MODULE=ibus kitty
``` ```
Wayland needs no environment variable. Start `strans` after a wlroots Wayland needs no environment variable. Start `strans` after a compositor
compositor that supports input-method-v2. Neither ibus-daemon nor fcitx is that supports input-method-v2. Neither ibus-daemon nor fcitx is required.
required. Starting without `DISPLAY` is supported: the optional X popup is disabled,
while input processing and non-X frontends continue to run.
## Keys ## Ownership and rendering constraints
Input modes are selected with Ctrl and one letter: 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.
```text The popup is an optional X window, including when the active frontend is
Ctrl+N Hiragana Wayland. It uses deterministic font fallback and fixed-width rune cells;
Ctrl+K Katakana U+FE0E and U+FE0F variation selectors use zero cells. It does not perform
Ctrl+S Hangul OpenType shaping, color-emoji rendering, or general grapheme clustering, and
Ctrl+T English there is no native Wayland popup renderer. Multi-rune emoji are committed
Ctrl+V Vietnamese Telex intact but are drawn as their constituent rune cells under this boundary. A
Ctrl+E Emoji and symbol search supplied caret rectangle is used when available, with the pointer as fallback.
Ctrl+P Toggle popup preedit The X renderer accepts only a 24-depth, 32-bpp TrueColor root format and
``` disables itself cleanly on other visuals.
Use `1`-`9` or the arrow keys to select a candidate. Tab moves through ## Data, third-party code, and licensing
candidates, Enter commits, and Escape cancels.
Emoji search is temporary and returns to the previous input mode: Data and third-party provenance is recorded in
[`docs/PROVENANCE.md`](docs/PROVENANCE.md), with font-specific details in
[`font/PROVENANCE`](font/PROVENANCE). 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).
```text The repository does not currently declare a license for the strans project
Ctrl+E → smile → Enter → 😀 source as a whole. The licenses of bundled data, fonts, generated protocol
``` code, and third-party components do not by themselves license the original
strans source.
## Tests
Tests use the sibling `cutest` repository as a submodule. Its relative URL
expects repository mirrors and forks to provide the same sibling. Initialize
it once, then run the checks in the build container:
```sh
git submodule update --init
make docker-check
```
The native `make check` command needs plan9port and Python 3, but not the GUI
development packages. Use `make test TESTARGS=hangul` or
`make docker-check TESTARGS=hangul` to filter unit tests. After editing
`map/emoji.src`, regenerate and verify the dictionary with:
```sh
map/mkemoji > map/emoji.dict
make verify-map
```
## Design
`strans` owns the input engine and candidate window. The XIM and GTK frontends
forward keys to it over a Unix socket; the IBus and Wayland frontends are built
into the daemon. Internal threads communicate through CSP channels.

View File

@@ -1,23 +1,43 @@
#!/bin/sh #!/bin/sh
pkill strans set -eu
pkill strans-xim
sleep 1 cd "$(dirname "$0")"
strans_pid=
perf_pid=
cleanup()
{
trap - 0 1 2 15
if test -n "$perf_pid"; then
kill -INT "$perf_pid" 2>/dev/null || true
wait "$perf_pid" 2>/dev/null || true
fi
if test -n "$strans_pid"; then
kill "$strans_pid" 2>/dev/null || true
wait "$strans_pid" 2>/dev/null || true
fi
}
trap cleanup 0 1 2 15
./strans map font & ./strans map font &
strans_pid=$!
sleep 1 sleep 1
kill -0 "$strans_pid"
# warm up glyph cache # warm up glyph cache
./bench/bench bench/bench.keys 100 ./bench/bench bench/bench.keys 100
echo "cache warmed up" echo "cache warmed up"
STRANS_PID=$(pgrep -x strans) perf record -g -o bench/perf.data -p "$strans_pid" &
perf record -g -o bench/perf.data -p "$STRANS_PID" & perf_pid=$!
PERF_PID=$!
sleep 1 sleep 1
kill -0 "$perf_pid"
./bench/bench bench/bench.keys 100 ./bench/bench bench/bench.keys 100
kill -INT $PERF_PID kill -INT "$perf_pid"
wait $PERF_PID 2>/dev/null wait "$perf_pid" 2>/dev/null || true
pkill strans perf_pid=

View File

@@ -1,11 +1,12 @@
#define _POSIX_C_SOURCE 200809L #define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <unistd.h> #include <unistd.h>
#include <time.h> #include <time.h>
#include <sys/socket.h>
#include <sys/un.h>
#include "../ipc.h" #include "../ipc.h"
typedef struct Key Key; typedef struct Key Key;
@@ -15,7 +16,7 @@ struct Key {
}; };
static Key *keys; static Key *keys;
static int nkeys; static size_t nkeys;
static int fd; static int fd;
static void static void
@@ -28,13 +29,19 @@ die(char *msg)
static void static void
addkey(int k, int mod) addkey(int k, int mod)
{ {
static int cap; static size_t cap;
Key *p;
if(nkeys >= cap){ if(nkeys >= cap){
if(cap > SIZE_MAX / 2 / sizeof(Key)){
fprintf(stderr, "too many keys\n");
exit(1);
}
cap = cap ? cap * 2 : 256; cap = cap ? cap * 2 : 256;
keys = realloc(keys, cap * sizeof(Key)); p = realloc(keys, cap * sizeof(Key));
if(!keys) if(!p)
die("realloc"); die("realloc");
keys = p;
} }
keys[nkeys].k = k; keys[nkeys].k = k;
keys[nkeys].mod = mod; keys[nkeys].mod = mod;
@@ -76,20 +83,35 @@ loadkeys(char *file)
} }
} }
fclose(f); fclose(f);
if(nkeys == 0){
fprintf(stderr, "%s: no keys\n", file);
exit(1);
}
}
static uint64_t
iterations(char *s)
{
uintmax_t n;
char *end;
if(*s == '\0' || *s == '-')
goto Bad;
errno = 0;
n = strtoumax(s, &end, 10);
if(errno != 0 || *end != '\0' || n == 0 || n > UINT64_MAX)
goto Bad;
return n;
Bad:
fprintf(stderr, "invalid iteration count: %s\n", s);
exit(1);
} }
static void static void
dial(void) dial(void)
{ {
struct sockaddr_un addr; fd = ipcconnect();
fd = socket(AF_UNIX, SOCK_STREAM, 0);
if(fd < 0) if(fd < 0)
die("socket");
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
snprintf(addr.sun_path, sizeof(addr.sun_path), IPCPATH, getuid());
if(connect(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0)
die("connect"); die("connect");
} }
@@ -124,31 +146,38 @@ now(void)
int int
main(int argc, char **argv) main(int argc, char **argv)
{ {
int i, j, niter; size_t j;
uint64_t i, niter, total;
double t0, t1, dt; double t0, t1, dt;
if(argc < 2){ if(argc != 2 && argc != 3){
fprintf(stderr, "usage: bench file [niter]\n"); fprintf(stderr, "usage: bench file [niter]\n");
exit(1); exit(1);
} }
loadkeys(argv[1]); loadkeys(argv[1]);
niter = argc > 2 ? atoi(argv[2]) : 1000; niter = argc == 3 ? iterations(argv[2]) : 1000;
if(nkeys > UINT64_MAX || niter > UINT64_MAX / nkeys){
fprintf(stderr, "iteration count overflows key total\n");
exit(1);
}
total = niter * nkeys;
dial(); dial();
t0 = now(); t0 = now();
for(i = 0; i < niter; i++) for(i = 0; i < niter; i++)
for(j = 0; j < nkeys; j++){ for(j = 0; j < nkeys; j++){
sendkey(keys[j].k, keys[j].mod); sendkey(keys[j].k, keys[j].mod);
if(readresp() < 0){ if(readresp() < 0){
fprintf(stderr, "failed at iter %d key %d\n", i, j); fprintf(stderr, "failed at iter %" PRIu64 " key %zu\n", i, j);
exit(1); exit(1);
} }
} }
t1 = now(); t1 = now();
dt = t1 - t0; dt = t1 - t0;
printf("%d iters x %d keys = %d keys\n", niter, nkeys, niter * nkeys); printf("%" PRIu64 " iters x %zu keys = %" PRIu64 " keys\n",
niter, nkeys, total);
printf("%.3f ms total, %.3f us/key, %.3f us/iter\n", printf("%.3f ms total, %.3f us/key, %.3f us/iter\n",
dt * 1000, dt * 1e6 / (niter * nkeys), dt * 1e6 / niter); dt * 1000, dt * 1e6 / total, dt * 1e6 / niter);
close(fd); close(fd);
return 0; return 0;
} }

Binary file not shown.

Binary file not shown.

36
dat.h
View File

@@ -17,7 +17,6 @@ enum
LangVI = 0x16, LangVI = 0x16,
Fontsz = 32, Fontsz = 32,
Fontbase = 4,
Nglyphs = 0x20000, Nglyphs = 0x20000,
Maxrunes = 64, Maxrunes = 64,
Maxutf = Maxrunes * UTFmax + 1, Maxutf = Maxrunes * UTFmax + 1,
@@ -28,6 +27,7 @@ enum
Maxclients = 64, Maxclients = 64,
Maxkouho = 32, Maxkouho = 32,
Maxdisp = 9, Maxdisp = 9,
Wlkeymax = 0x300,
Imgw = (Maxrunes + 3) * Fontsz, Imgw = (Maxrunes + 3) * Fontsz,
Imgh = (Maxdisp + 1) * Fontsz, Imgh = (Maxdisp + 1) * Fontsz,
@@ -43,6 +43,17 @@ struct Str
int n; int n;
}; };
typedef struct Wlstate Wlstate;
struct Wlstate
{
uchar down[Wlkeymax];
u32int repeatkey;
uvlong repeatat;
int rate;
int delay;
int repeating;
};
typedef struct Emit Emit; typedef struct Emit Emit;
struct Emit struct Emit
{ {
@@ -110,23 +121,41 @@ struct Im
{ {
Lang *l; Lang *l;
Str pre; Str pre;
Str raw; /* physical keys for the current Telex preedit */ /* Telex history, or the short pending romaji in a Japanese mode. */
Str raw;
Str kouho[Maxkouho]; Str kouho[Maxkouho];
int nkouho; int nkouho;
int sel; int sel;
}; };
typedef struct Drawcmd Drawcmd; typedef struct Drawcmd Drawcmd;
typedef struct Caret Caret;
struct Caret
{
int valid;
int x;
int y;
int h;
};
struct Drawcmd struct Drawcmd
{ {
Str pre; Str pre;
Str kouho[Maxdisp]; Str kouho[Maxdisp];
int nkouho; int nkouho;
int sel; int sel;
Caret caret;
}; };
typedef struct Keyreq Keyreq; typedef struct Keyreq Keyreq;
typedef struct Keyres Keyres; typedef struct Keyres Keyres;
enum
{
Keypress,
Keyreset,
Keyrelease,
Keycaret,
};
struct Keyres struct Keyres
{ {
int eaten; int eaten;
@@ -136,8 +165,11 @@ struct Keyres
struct Keyreq struct Keyreq
{ {
uvlong owner;
int op;
u32int ks; u32int ks;
u32int mod; u32int mod;
Caret caret;
int want; /* nonzero: include preedit in reply */ int want; /* nonzero: include preedit in reply */
Channel *reply; Channel *reply;
}; };

35
dict.c
View File

@@ -33,7 +33,7 @@ dictlookup(Dictreq *req, Dictres *res)
while(p < e && *p != ' ') while(p < e && *p != ' ')
p++; p++;
sinit(&tmp, sp, p - sp); sinit(&tmp, sp, p - sp);
if(scmp(&tmp, &req->key) != 0) if(req->lang == LangEMOJI || scmp(&tmp, &req->key) != 0)
res->kouho[res->nkouho++] = tmp; res->kouho[res->nkouho++] = tmp;
if(p < e) if(p < e)
p++; p++;
@@ -44,7 +44,7 @@ void
dictthread(void*) dictthread(void*)
{ {
Dictreq req; Dictreq req;
Dictres res; static Dictres res;
threadsetname("dict"); threadsetname("dict");
for(;;){ for(;;){
@@ -63,14 +63,16 @@ dictopen(char *path)
Hmap *h; Hmap *h;
Biobuf *b; Biobuf *b;
Str key; Str key;
char *line, *tab; char *line, *tab, *p, *e;
int len; int len, lineno;
b = Bopen(path, OREAD); b = Bopen(path, OREAD);
if(b == nil) if(b == nil)
die("can't open: %s", path); die("can't open: %s", path);
h = hmapalloc(4096); h = hmapalloc(4096);
lineno = 0;
while((line = Brdstr(b, '\n', 1)) != nil){ while((line = Brdstr(b, '\n', 1)) != nil){
lineno++;
len = strlen(line); len = strlen(line);
if(len > 0 && line[len-1] == '\r') if(len > 0 && line[len-1] == '\r')
line[--len] = '\0'; line[--len] = '\0';
@@ -79,11 +81,21 @@ dictopen(char *path)
continue; continue;
} }
tab = strchr(line, '\t'); tab = strchr(line, '\t');
if(tab == nil || tab >= line + len - 1){ if(tab == nil || tab == line || tab >= line + len - 1 ||
free(line); strchr(tab+1, '\t') != nil)
continue; die("malformed dictionary: %s:%d", path, lineno);
}
*tab = '\0'; *tab = '\0';
if(utflen(line) > Maxrunes)
die("dictionary key too long: %s:%d", path, lineno);
for(p = tab+1; p < line+len; p = e+1){
e = memchr(p, ' ', line+len-p);
if(e == nil)
e = line+len;
if(utfnlen(p, e-p) > Maxrunes)
die("dictionary candidate too long: %s:%d", path, lineno);
if(e == line+len)
break;
}
sinit(&key, line, tab - line); sinit(&key, line, tab - line);
hmapset(&h, &key, tab+1, len - (tab - line) - 1); hmapset(&h, &key, tab+1, len - (tab - line) - 1);
free(line); free(line);
@@ -95,13 +107,16 @@ dictopen(char *path)
void void
dictinit(char *dir) dictinit(char *dir)
{ {
char path[1024]; char *path;
int i; int i;
for(i = 0; i < nlang; i++){ for(i = 0; i < nlang; i++){
if(langs[i].dictname == nil) if(langs[i].dictname == nil)
continue; continue;
snprint(path, sizeof(path), "%s/%s.dict", dir, langs[i].dictname); path = smprint("%s/%s.dict", dir, langs[i].dictname);
if(path == nil)
die("out of memory");
langs[i].dict = dictopen(path); langs[i].dict = dictopen(path);
free(path);
} }
} }

72
docs/PROVENANCE.md Normal file
View File

@@ -0,0 +1,72 @@
# Data and third-party provenance
This file records what can be established from repository contents and
history. It does not assign a license to original strans source code.
## Input maps and dictionaries
`map/hira.map`, `map/kata.map`, and the original map data entered the history
in commit `cc157c0b3c33506d1cbf5bd251ddc6a9ca909081`, whose subject says the
maps came from 9front. That commit does not identify a 9front revision or
carry a separate data license. `map/telex.map` is generated by
`map/mktelex.py`; Vietnamese behavior is retained for compatibility.
`map/emoji.dict` is generated deterministically from the repository-maintained
`map/emoji.src` by `map/mkemoji`. No external dataset is required.
`map/kanji.dict` identifies itself as SKK Medium dictionary version 8.1,
May 24, 1995, rearranged for Plan 9 ktrans by Kenji Okamoto on February 17,
2000. Its header names its copyright holders and grants GPL version 2 or
later. The complete GPL version 2 text is in
`LICENSES/GPL-2.0-or-later.txt`; the dictionary header supplies the
"or later" option. `map/README` records the exact repository history,
repairs made to this copy, and the reproducible procedure for importing a
new SKK revision.
## Fonts
The popup bundles Noto Sans, Japanese-region Noto Sans Mono CJK, and Noto
Emoji. Exact checksums, versions or source revisions, download locations
where known, fallback order, and license-file names are recorded in
`font/PROVENANCE`. The corresponding OFL 1.1 and Apache 2.0 texts are kept
beside the fonts.
## stb_truetype
`stb_truetype.h` is Sean Barrett's stb_truetype version 1.26, dated
August 28, 2021 in its header. Its exact repository checksum is:
```text
ecd30b05e0dd4fea3a13c26810dd9e1992dc379049482c393d5a19e6b5090aab
```
The file was present in the repository's first commit,
`dcd1147638f908eca7e4b7616815e215022cc99d`. No upstream commit or download
URL was recorded. Its complete dual MIT/Public Domain terms are retained at
the end of the header.
## Generated Wayland protocol code
The four checked-in Wayland protocol C/header files were added in commit
`7629ec5fe8976812655c9d12b447a62abc8bb671`:
- `input-method-unstable-v2-client-protocol.h`
- `input-method-unstable-v2-protocol.c`
- `virtual-keyboard-unstable-v1-client-protocol.h`
- `virtual-keyboard-unstable-v1-protocol.c`
Their embedded notices identify the protocols as input-method-unstable-v2
and virtual-keyboard-unstable-v1 and include the applicable MIT-style license
terms. The input-method files report generation by wayland-scanner 1.21.0;
the virtual-keyboard files report wayland-scanner 1.23.1. The source XML
files and their upstream repository revisions were not committed or recorded
in history, so these generated files cannot honestly be regenerated
byte-for-byte from repository artifacts. Do not edit them manually. A
future regeneration should commit the source XML or record its immutable
upstream revision and the scanner command/version.
## Git submodules
`cutest` is pinned by the repository as the unit-test framework. XIM uses
the system xcb-imdkit and xcb-util libraries at build time; those system
libraries are not bundled by this repository.

13
fn.h
View File

@@ -28,6 +28,8 @@ void dictthread(void*);
void dictlookup(Dictreq*, Dictres*); void dictlookup(Dictreq*, Dictres*);
void drawthread(void*); void drawthread(void*);
int popupcells(Rune*, int);
void popupposition(Caret*, int, int, int, int, int, int, int*, int*);
void imthread(void*); void imthread(void*);
Emit transmap(Im*, Rune); Emit transmap(Im*, Rune);
Emit transko(Im*, Rune); Emit transko(Im*, Rune);
@@ -35,14 +37,23 @@ Emit transvi(Im*, Rune);
void backko(Im*); void backko(Im*);
void backvi(Im*); void backvi(Im*);
void dictsend(Im*, Str*); void dictsend(Im*, Str*);
uvlong ownernew(void);
int srvreadkey(int, Keyreq*); int srvreadkey(int, Keyreq*);
void srvthread(void*); void srvthread(void*);
void ibusthread(void*); void ibusthread(void*);
void waylandthread(void*); void waylandthread(void*);
void wlstateinit(Wlstate*);
int wlstatepress(Wlstate*, u32int, int, int, uvlong);
int wlstaterelease(Wlstate*, u32int);
void wlstaterepeatinfo(Wlstate*, int, int);
void wlstatecancelrepeat(Wlstate*);
void wlstateclear(Wlstate*);
int wlstatetimeout(Wlstate*, uvlong);
int wlstaterepeat(Wlstate*, uvlong, u32int*);
void* emalloc(ulong); void* emalloc(ulong);
void* erealloc(void*, ulong); void* erealloc(void*, ulong);
void fontinit(char*); int fontinit(char*);
void putfont(u32int*, int, int, int, int, Rune); void putfont(u32int*, int, int, int, int, Rune);

130
font.c
View File

@@ -4,95 +4,111 @@
#include "fn.h" #include "fn.h"
typedef struct Glyph Glyph; typedef struct Glyph Glyph;
typedef struct Font Font;
struct Glyph struct Glyph
{ {
uchar *bmp; uchar *bmp;
int w, h, ox, oy; int w, h, ox, oy, base;
};
struct Font
{
stbtt_fontinfo info;
uchar *data;
float scale;
int base;
}; };
enum { Maxfonts = 4 }; enum { Maxfonts = 4 };
static stbtt_fontinfo fonts[Maxfonts]; static Font fonts[Maxfonts];
static uchar *fontdata[Maxfonts];
static float scale[Maxfonts];
static int nfonts; static int nfonts;
static Glyph cache[Nglyphs]; static Glyph cache[Nglyphs];
static u32int blendtab[2][256]; static u32int blendtab[2][256];
static u32int static u32int
blend(u32int bg, int a) blend(u32int bg, u32int fg, int a)
{ {
int r, g, b, inv; int r, g, b, inv;
inv = 255 - a; inv = 255 - a;
r = (bg >> 16 & 0xff) * inv / 255; r = ((bg >> 16 & 0xff) * inv + (fg >> 16 & 0xff) * a) / 255;
g = (bg >> 8 & 0xff) * inv / 255; g = ((bg >> 8 & 0xff) * inv + (fg >> 8 & 0xff) * a) / 255;
b = (bg & 0xff) * inv / 255; b = ((bg & 0xff) * inv + (fg & 0xff) * a) / 255;
return (r << 16) | (g << 8) | b; return (r << 16) | (g << 8) | b;
} }
static void static int
loadfont(char *path) loadfont(char *path)
{ {
int fd; Font *f;
int ascent, descent, gap, fd;
long sz, n; long sz, n;
if(nfonts >= Maxfonts)
die("too many fonts");
fd = open(path, OREAD); fd = open(path, OREAD);
if(fd < 0) if(fd < 0){
die("can't open font: %s", path); fprint(2, "strans: popup: can't open font: %s\n", path);
return 0;
}
sz = seek(fd, 0, 2); sz = seek(fd, 0, 2);
seek(fd, 0, 0); seek(fd, 0, 0);
fontdata[nfonts] = emalloc(sz); if(sz <= 0){
n = readn(fd, fontdata[nfonts], sz); fprint(2, "strans: popup: empty font: %s\n", path);
close(fd); close(fd);
if(n != sz)
die("can't read font: %s", path);
if(!stbtt_InitFont(&fonts[nfonts], fontdata[nfonts], stbtt_GetFontOffsetForIndex(fontdata[nfonts], 0)))
die("can't init font: %s", path);
scale[nfonts] = stbtt_ScaleForPixelHeight(&fonts[nfonts], Fontsz);
nfonts++;
}
static int
isfont(char *name)
{
char *p;
p = strrchr(name, '.');
if(p == nil)
return 0; return 0;
return strcmp(p, ".ttf") == 0 || strcmp(p, ".otf") == 0; }
f = &fonts[nfonts];
f->data = emalloc(sz);
n = readn(fd, f->data, sz);
close(fd);
if(n != sz){
fprint(2, "strans: popup: can't read font: %s\n", path);
free(f->data);
f->data = nil;
return 0;
}
if(!stbtt_InitFont(&f->info, f->data,
stbtt_GetFontOffsetForIndex(f->data, 0))){
fprint(2, "strans: popup: can't init font: %s\n", path);
free(f->data);
f->data = nil;
return 0;
}
f->scale = stbtt_ScaleForPixelHeight(&f->info, Fontsz);
stbtt_GetFontVMetrics(&f->info, &ascent, &descent, &gap);
(void)descent;
(void)gap;
f->base = (int)(ascent * f->scale);
nfonts++;
return 1;
} }
void int
fontinit(char *dir) fontinit(char *dir)
{ {
int fd, n, i, a; static char *names[] = {
Dir *d; "NotoSans-Regular.ttf",
"NotoSansMonoCJKjp-Regular.otf",
"NotoEmoji-Regular.ttf",
};
int i, a;
char path[256]; char path[256];
fd = open(dir, OREAD); nfonts = 0;
if(fd < 0) memset(cache, 0, sizeof cache);
die("can't open font dir: %s", dir); for(i = 0; i < nelem(names); i++){
n = dirreadall(fd, &d); if(strlen(dir) + 1 + strlen(names[i]) + 1 > sizeof path){
close(fd); fprint(2, "strans: popup: font path is too long\n");
if(n < 0) continue;
die("can't read font dir: %s", dir); }
for(i = 0; i < n; i++){ snprint(path, sizeof path, "%s/%s", dir, names[i]);
if(isfont(d[i].name)){
snprint(path, sizeof path, "%s/%s", dir, d[i].name);
loadfont(path); loadfont(path);
} }
}
free(d);
if(nfonts == 0)
die("no fonts in %s", dir);
for(a = 0; a < 256; a++){ for(a = 0; a < 256; a++){
blendtab[0][a] = blend(Colbg, a); blendtab[0][a] = blend(Colbg, Colfg, a);
blendtab[1][a] = blend(Colsel, a); blendtab[1][a] = blend(Colsel, Colfg, a);
} }
return nfonts != 0;
} }
void void
@@ -108,17 +124,21 @@ putfont(u32int *buf, int w, int h, int px, int py, Rune r)
g = &cache[r]; g = &cache[r];
if(g->bmp == nil){ if(g->bmp == nil){
for(f = 0; f < nfonts; f++){ for(f = 0; f < nfonts; f++){
if(stbtt_FindGlyphIndex(&fonts[f], r) == 0) if(stbtt_FindGlyphIndex(&fonts[f].info, r) == 0)
continue; continue;
g->bmp = stbtt_GetCodepointBitmap(&fonts[f], scale[f], scale[f], r, &g->w, &g->h, &g->ox, &g->oy); g->bmp = stbtt_GetCodepointBitmap(&fonts[f].info,
if(g->bmp != nil) fonts[f].scale, fonts[f].scale, r,
&g->w, &g->h, &g->ox, &g->oy);
if(g->bmp != nil){
g->base = fonts[f].base;
break; break;
} }
}
if(g->bmp == nil) if(g->bmp == nil)
return; return;
} }
y0 = py + g->oy + Fontsz - Fontbase; y0 = py + g->base + g->oy;
j0 = y0 < 0 ? -y0 : 0; j0 = y0 < 0 ? -y0 : 0;
j1 = y0 + g->h > h ? h - y0 : g->h; j1 = y0 + g->h > h ? h - y0 : g->h;
x0 = px + g->ox; x0 = px + g->ox;

View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

94
font/OFL-NotoSans.txt Normal file
View File

@@ -0,0 +1,94 @@
Copyright 2018 The Noto Project Authors (github.com/googlei18n/noto-fonts)
This Font Software is licensed under the SIL Open Font License,
Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font
creation efforts of academic and linguistic communities, and to
provide a free and open framework in which fonts may be shared and
improved in partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply to
any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software
components as distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to,
deleting, or substituting -- in part or in whole -- any of the
components of the Original Version, by changing formats or by porting
the Font Software to a new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed,
modify, redistribute, and sell modified and unmodified copies of the
Font Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components, in
Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the
corresponding Copyright Holder. This restriction only applies to the
primary font name as presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created using
the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

92
font/OFL-NotoSansCJK.txt Normal file
View File

@@ -0,0 +1,92 @@
This Font Software is licensed under the SIL Open Font License,
Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font
creation efforts of academic and linguistic communities, and to
provide a free and open framework in which fonts may be shared and
improved in partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply to
any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software
components as distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to,
deleting, or substituting -- in part or in whole -- any of the
components of the Original Version, by changing formats or by porting
the Font Software to a new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed,
modify, redistribute, and sell modified and unmodified copies of the
Font Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components, in
Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the
corresponding Copyright Holder. This restriction only applies to the
primary font name as presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created using
the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

37
font/PROVENANCE Normal file
View File

@@ -0,0 +1,37 @@
Bundled popup fonts
===================
The renderer tries these files in this fixed order:
1. NotoSans-Regular.ttf
2. NotoSansMonoCJKjp-Regular.otf
3. NotoEmoji-Regular.ttf
NotoSans-Regular.ttf
--------------------
SHA-256: 9cb49a54e520423033f9727be2e53e4805a60656deb09c219740d8e5f3e033ac
Internal family/version: Noto Sans Regular, version 2.005
License: SIL Open Font License 1.1 (OFL-NotoSans.txt)
This file entered the strans repository in commit 874b941. That commit did
not record its download URL, and the current upstream build is not
byte-identical. The checksum above identifies the bundled file; no claim is
made that the current upstream URL regenerates it.
NotoSansMonoCJKjp-Regular.otf
--------------------------------
SHA-256: 4d01725be822d144cf9a56ade981e6fb920cd7a610b8fc24cc601a920beea5b9
Release: Noto Sans CJK 2.004, tag Sans2.004
Source: https://raw.githubusercontent.com/notofonts/noto-cjk/Sans2.004/Sans/Mono/NotoSansMonoCJKjp-Regular.otf
License: SIL Open Font License 1.1 (OFL-NotoSansCJK.txt)
License source: https://raw.githubusercontent.com/notofonts/noto-cjk/Sans2.004/LICENSE
NotoEmoji-Regular.ttf
---------------------
SHA-256: 415dc6290378574135b64c808dc640c1df7531973290c4970c51fdeb849cb0c5
Source: https://raw.githubusercontent.com/googlei18n/noto-emoji/2f1ffdd6fbbd05d6f382138a3d3adcd89c5ce800/fonts/NotoEmoji-Regular.ttf
License: Apache License 2.0 (Apache-2.0-NotoEmoji.txt)
License source: https://raw.githubusercontent.com/googlei18n/noto-emoji/2f1ffdd6fbbd05d6f382138a3d3adcd89c5ce800/LICENSE

View File

@@ -1,8 +1,8 @@
PROG = im-strans.so PROG = im-strans.so
CFLAGS = -Wall -O2 -I.. $(shell pkg-config --cflags gtk+-3.0) CFLAGS = -Wall -O2 -I.. $(shell pkg-config --cflags gtk+-3.0)
LDFLAGS = $(shell pkg-config --libs gtk+-3.0) LDFLAGS = $(shell pkg-config --libs gtk+-3.0)
DSTDIR ?= $(shell find /usr/lib* -type d \ LIBDIR ?= /usr/lib
-path "*/gtk-3.0/*/immodules" 2>/dev/null | head -1) GTK_MODULE_DIR = $(LIBDIR)/gtk-3.0/3.0.0/immodules
all: $(PROG) all: $(PROG)
@@ -10,15 +10,15 @@ $(PROG): main.c ../ipc.c ../ipc.h
$(CC) -shared -fPIC $(CFLAGS) -o $@ main.c ../ipc.c $(LDFLAGS) $(CC) -shared -fPIC $(CFLAGS) -o $@ main.c ../ipc.c $(LDFLAGS)
install: $(PROG) install: $(PROG)
test -n "$(DSTDIR)" test -n "$(LIBDIR)"
mkdir -p "$(DSTDIR)" mkdir -p "$(DESTDIR)$(GTK_MODULE_DIR)"
cp $(PROG) "$(DSTDIR)/" cp $(PROG) "$(DESTDIR)$(GTK_MODULE_DIR)/"
gtk-query-immodules-3.0 --update-cache if test -z "$(DESTDIR)"; then gtk-query-immodules-3.0 --update-cache; fi
uninstall: uninstall:
test -n "$(DSTDIR)" test -n "$(LIBDIR)"
rm -f "$(DSTDIR)/$(PROG)" rm -f "$(DESTDIR)$(GTK_MODULE_DIR)/$(PROG)"
gtk-query-immodules-3.0 --update-cache if test -z "$(DESTDIR)"; then gtk-query-immodules-3.0 --update-cache; fi
clean: clean:
rm -f $(PROG) rm -f $(PROG)

View File

@@ -2,8 +2,6 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <unistd.h> #include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <gtk/gtk.h> #include <gtk/gtk.h>
#include "ipc.h" #include "ipc.h"
@@ -27,20 +25,9 @@ static GType imtype;
static void static void
srvconnect(Im *im) srvconnect(Im *im)
{ {
struct sockaddr_un addr;
if(im->fd >= 0) if(im->fd >= 0)
return; return;
im->fd = socket(AF_UNIX, SOCK_STREAM, 0); im->fd = ipcconnect();
if(im->fd < 0)
return;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
snprintf(addr.sun_path, sizeof(addr.sun_path), IPCPATH, getuid());
if(connect(im->fd, (struct sockaddr*)&addr, sizeof(addr)) < 0){
close(im->fd);
im->fd = -1;
}
} }
static void static void
@@ -120,7 +107,7 @@ sendreset(Im *im)
srvclose(im); srvclose(im);
return; return;
} }
ipcpackreq(buf, 1, 0, Kesc); ipcpackreset(buf, 1);
if(ipcsend(im->fd, buf, sizeof buf) < 0 || if(ipcsend(im->fd, buf, sizeof buf) < 0 ||
readresp(im, resp, sizeof(resp)) < 0) readresp(im, resp, sizeof(resp)) < 0)
srvclose(im); srvclose(im);
@@ -216,7 +203,12 @@ reset(GtkIMContext *ctx)
static void static void
focusout(GtkIMContext *ctx) focusout(GtkIMContext *ctx)
{ {
sendreset((Im*)ctx); Im *im;
im = (Im*)ctx;
sendreset(im);
/* Closing the context connection releases engine ownership. */
srvclose(im);
} }
static void static void

14
hash.c
View File

@@ -25,7 +25,7 @@ hmapalloc(int nbuckets)
int nsz; int nsz;
if(nbuckets < 1) if(nbuckets < 1)
die("hmapalloc: no buckets"); return nil;
nsz = Tagsize; nsz = Tagsize;
store = emalloc(sizeof(*h) + nbuckets * nsz); store = emalloc(sizeof(*h) + nbuckets * nsz);
h = store; h = store;
@@ -54,6 +54,8 @@ hmapget(Hmap *h, Str *key)
Hnode *n; Hnode *n;
uchar *v; uchar *v;
if(h == nil || key == nil || key->n < 0 || key->n > Maxrunes)
return nil;
v = h->nodes + (hash(key) % h->nbs) * h->nsz; v = h->nodes + (hash(key) % h->nbs) * h->nsz;
for(;;){ for(;;){
n = (Hnode*)v; n = (Hnode*)v;
@@ -67,7 +69,7 @@ hmapget(Hmap *h, Str *key)
} }
static char* static char*
sdup(Str *s) sdup(Str *s, int *len)
{ {
char buf[Maxutf]; char buf[Maxutf];
char *p; char *p;
@@ -77,6 +79,7 @@ sdup(Str *s)
p = emalloc(n + 1); p = emalloc(n + 1);
memmove(p, buf, n); memmove(p, buf, n);
p[n] = '\0'; p[n] = '\0';
*len = n;
return p; return p;
} }
@@ -121,6 +124,10 @@ hmapset(Hmap **store, Str *key, const char *val, int vlen)
int next; int next;
vlong diff; vlong diff;
if(store == nil || *store == nil || key == nil ||
key->n < 0 || key->n > Maxrunes || vlen < 0 ||
(vlen > 0 && val == nil))
return;
newval = memdup(val, vlen); newval = memdup(val, vlen);
h = *store; h = *store;
v = h->nodes + (hash(key) % h->nbs) * h->nsz; v = h->nodes + (hash(key) % h->nbs) * h->nsz;
@@ -151,8 +158,7 @@ hmapset(Hmap **store, Str *key, const char *val, int vlen)
n = (Hnode*)v; n = (Hnode*)v;
replace: replace:
if(n->filled == 0){ if(n->filled == 0){
n->key = sdup(key); n->key = sdup(key, &n->klen);
n->klen = strlen(n->key);
n->filled = 1; n->filled = 1;
} }
n->next = next; n->next = next;

298
ibus.c
View File

@@ -14,7 +14,9 @@ enum
{ {
Maxwatches = 32, Maxwatches = 32,
Maxconns = 16, Maxconns = 16,
Maxcontexts = 64,
Relmask = 1<<30, Relmask = 1<<30,
Ibussupermask = 1<<26,
}; };
typedef struct Watch Watch; typedef struct Watch Watch;
@@ -23,12 +25,26 @@ struct Watch
DBusWatch *w; DBusWatch *w;
}; };
typedef struct Ictx Ictx;
struct Ictx
{
DBusConnection *conn;
char path[64];
uvlong owner;
int focused;
Caret caret;
};
static Watch watches[Maxwatches]; static Watch watches[Maxwatches];
static int nwatches; static int nwatches;
static DBusConnection *conns[Maxconns]; static DBusConnection *conns[Maxconns];
static int nconns; static int nconns;
static DBusServer *srv; static DBusServer *srv;
static char addrfile[256]; static Ictx contexts[Maxcontexts];
static char addrfile[512];
static dev_t addrdev;
static ino_t addrino;
static int addrowned;
static int icctr; static int icctr;
static int busctr; static int busctr;
static Channel *replyc; static Channel *replyc;
@@ -38,8 +54,12 @@ static DBusHandlerResult onmsg(DBusConnection*, DBusMessage*, void*);
static void static void
unlinkaddr(void) unlinkaddr(void)
{ {
if(addrfile[0] != '\0') struct stat st;
if(addrowned && lstat(addrfile, &st) == 0 &&
st.st_dev == addrdev && st.st_ino == addrino)
unlink(addrfile); unlink(addrfile);
addrowned = 0;
} }
static void static void
@@ -97,7 +117,8 @@ buildaddrpath(char *buf, int sz)
{ {
char mid[64], host[64], num[8], *cfg, *home; char mid[64], host[64], num[8], *cfg, *home;
struct stat st; struct stat st;
char dir[256]; char dir[512];
int n;
machineid(mid, sizeof(mid)); machineid(mid, sizeof(mid));
if(mid[0] == '\0') if(mid[0] == '\0')
@@ -105,15 +126,17 @@ buildaddrpath(char *buf, int sz)
xdisplay(host, sizeof(host), num, sizeof(num)); xdisplay(host, sizeof(host), num, sizeof(num));
cfg = getenv("XDG_CONFIG_HOME"); cfg = getenv("XDG_CONFIG_HOME");
if(cfg != nil && cfg[0] != '\0') if(cfg != nil && cfg[0] != '\0')
snprintf(dir, sizeof(dir), "%s/ibus/bus", cfg); n = snprintf(dir, sizeof(dir), "%s/ibus/bus", cfg);
else{ else{
home = getenv("HOME"); home = getenv("HOME");
if(home == nil) if(home == nil)
return -1; return -1;
snprintf(dir, sizeof(dir), "%s/.config/ibus/bus", home); n = snprintf(dir, sizeof(dir), "%s/.config/ibus/bus", home);
} }
if(n < 0 || n >= (int)sizeof dir)
return -1;
if(stat(dir, &st) < 0){ if(stat(dir, &st) < 0){
char tmp[256]; char tmp[512];
char *p; char *p;
strncpy(tmp, dir, sizeof(tmp)); strncpy(tmp, dir, sizeof(tmp));
tmp[sizeof(tmp)-1] = '\0'; tmp[sizeof(tmp)-1] = '\0';
@@ -125,23 +148,55 @@ buildaddrpath(char *buf, int sz)
} }
mkdir(tmp, 0700); mkdir(tmp, 0700);
} }
snprintf(buf, sz, "%s/%s-%s-%s", dir, mid, host, num); n = snprintf(buf, sz, "%s/%s-%s-%s", dir, mid, host, num);
return 0; return n < 0 || n >= sz ? -1 : 0;
} }
static int static int
writeaddr(char *path, char *addr) writeaddr(char *path, char *addr)
{ {
char tmp[576];
FILE *fp; FILE *fp;
struct stat st;
int fd, n, ok;
fp = fopen(path, "w"); n = snprintf(tmp, sizeof tmp, "%s.tmp.XXXXXX", path);
if(fp == nil) if(n < 0 || n >= (int)sizeof tmp)
return -1; return -1;
fprintf(fp, "IBUS_ADDRESS=%s\n", addr); fd = mkstemp(tmp);
fprintf(fp, "IBUS_DAEMON_PID=%d\n", (int)getpid()); if(fd < 0)
return -1;
ok = 0;
fp = nil;
if(fchmod(fd, 0600) < 0)
goto out;
fp = fdopen(fd, "w");
if(fp == nil)
goto out;
fd = -1;
if(fprintf(fp, "IBUS_ADDRESS=%s\n", addr) < 0 ||
fprintf(fp, "IBUS_DAEMON_PID=%d\n", (int)getpid()) < 0 ||
fflush(fp) < 0 || fsync(fileno(fp)) < 0)
goto out;
if(fclose(fp) < 0){
fp = nil;
goto out;
}
fp = nil;
if(rename(tmp, path) < 0 || lstat(path, &st) < 0)
goto out;
addrdev = st.st_dev;
addrino = st.st_ino;
addrowned = 1;
ok = 1;
out:
if(fp != nil)
fclose(fp); fclose(fp);
chmod(path, 0600); if(fd >= 0)
return 0; close(fd);
if(!ok)
unlink(tmp);
return ok ? 0 : -1;
} }
static dbus_bool_t static dbus_bool_t
@@ -203,29 +258,72 @@ mget(u32int state)
if(state & (1<<0)) m |= Mshift; if(state & (1<<0)) m |= Mshift;
if(state & (1<<2)) m |= Mctrl; if(state & (1<<2)) m |= Mctrl;
if(state & (1<<3)) m |= Malt; if(state & (1<<3)) m |= Malt;
if(state & (1<<6)) m |= Msuper; if(state & ((1<<6)|Ibussupermask)) m |= Msuper;
return m; return m;
} }
static Ictx*
findcontext(DBusConnection *conn, const char *path)
{
int i;
for(i = 0; i < nelem(contexts); i++)
if(contexts[i].conn == conn && strcmp(contexts[i].path, path) == 0)
return &contexts[i];
return nil;
}
static Ictx*
newcontext(DBusConnection *conn, const char *path)
{
int i;
for(i = 0; i < nelem(contexts); i++)
if(contexts[i].conn == nil){
memset(&contexts[i], 0, sizeof contexts[i]);
contexts[i].conn = conn;
contexts[i].owner = ownernew();
strncpy(contexts[i].path, path, sizeof contexts[i].path);
contexts[i].path[sizeof contexts[i].path-1] = '\0';
return &contexts[i];
}
return nil;
}
static void static void
sendkey(u32int ks, u32int mod, Keyres *res) sendrequest(Ictx *ctx, int op, u32int ks, u32int mod, Keyres *res)
{ {
Keyreq kr; Keyreq kr;
memset(&kr, 0, sizeof kr);
kr.owner = ctx->owner;
kr.op = op;
kr.ks = ks; kr.ks = ks;
kr.mod = mod; kr.mod = mod;
kr.want = 1; kr.caret = ctx->caret;
kr.want = op == Keypress;
kr.reply = replyc; kr.reply = replyc;
chansend(keyc, &kr); chansend(keyc, &kr);
chanrecv(replyc, res); chanrecv(replyc, res);
} }
static void static void
sendreset(void) releasecontext(Ictx *ctx)
{ {
Keyres res; Keyres res;
sendkey(Kesc, 0, &res); if(replyc != nil)
sendrequest(ctx, Keyrelease, 0, 0, &res);
ctx->focused = 0;
memset(&ctx->caret, 0, sizeof ctx->caret);
}
static void
dropcontext(Ictx *ctx)
{
releasecontext(ctx);
memset(ctx, 0, sizeof *ctx);
} }
static void static void
@@ -308,15 +406,53 @@ handlehello(DBusConnection *c, DBusMessage *m)
return DBUS_HANDLER_RESULT_HANDLED; return DBUS_HANDLER_RESULT_HANDLED;
} }
static DBusHandlerResult
handleerror(DBusConnection *c, DBusMessage *m, const char *name,
const char *text)
{
DBusMessage *r;
r = dbus_message_new_error(m, name, text);
if(r != nil){
dbus_connection_send(c, r, nil);
dbus_message_unref(r);
}
return DBUS_HANDLER_RESULT_HANDLED;
}
static DBusHandlerResult
handlebool(DBusConnection *c, DBusMessage *m, int value)
{
DBusMessage *r;
dbus_bool_t b;
r = dbus_message_new_method_return(m);
b = value ? TRUE : FALSE;
dbus_message_append_args(r, DBUS_TYPE_BOOLEAN, &b, DBUS_TYPE_INVALID);
dbus_connection_send(c, r, nil);
dbus_message_unref(r);
return DBUS_HANDLER_RESULT_HANDLED;
}
static DBusHandlerResult static DBusHandlerResult
handlecreate(DBusConnection *c, DBusMessage *m) handlecreate(DBusConnection *c, DBusMessage *m)
{ {
DBusMessage *r; DBusMessage *r;
char path[64]; char path[64];
const char *pp; const char *pp;
Ictx *ctx;
int n;
icctr++; icctr++;
snprintf(path, sizeof(path), "/org/freedesktop/IBus/InputContext_%d", icctr); n = snprintf(path, sizeof(path),
"/org/freedesktop/IBus/InputContext_%d", icctr);
if(n < 0 || n >= (int)sizeof path)
return handleerror(c, m, DBUS_ERROR_LIMITS_EXCEEDED,
"input context path exhausted");
ctx = newcontext(c, path);
if(ctx == nil)
return handleerror(c, m, DBUS_ERROR_LIMITS_EXCEEDED,
"too many input contexts");
pp = path; pp = path;
r = dbus_message_new_method_return(m); r = dbus_message_new_method_return(m);
dbus_message_append_args(r, DBUS_TYPE_OBJECT_PATH, &pp, DBUS_TYPE_INVALID); dbus_message_append_args(r, DBUS_TYPE_OBJECT_PATH, &pp, DBUS_TYPE_INVALID);
@@ -326,14 +462,12 @@ handlecreate(DBusConnection *c, DBusMessage *m)
} }
static DBusHandlerResult static DBusHandlerResult
handlekey(DBusConnection *c, DBusMessage *m) handlekey(DBusConnection *c, DBusMessage *m, Ictx *ctx)
{ {
DBusMessage *r;
DBusError err; DBusError err;
dbus_uint32_t sym, code, state; dbus_uint32_t sym, code, state;
Keyres res; Keyres res;
char commit[Maxutf], preedit[Maxutf]; char commit[Maxutf], preedit[Maxutf];
dbus_bool_t b;
u32int ks, mod; u32int ks, mod;
dbus_error_init(&err); dbus_error_init(&err);
@@ -346,28 +480,17 @@ handlekey(DBusConnection *c, DBusMessage *m)
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
} }
USED(code); USED(code);
if(state & Relmask){ if(state & Relmask || !ctx->focused)
r = dbus_message_new_method_return(m); return handlebool(c, m, 0);
b = FALSE;
dbus_message_append_args(r, DBUS_TYPE_BOOLEAN, &b, DBUS_TYPE_INVALID);
dbus_connection_send(c, r, nil);
dbus_message_unref(r);
return DBUS_HANDLER_RESULT_HANDLED;
}
ks = kget(sym); ks = kget(sym);
mod = mget(state); mod = mget(state);
sendkey(ks, mod, &res); sendrequest(ctx, Keypress, ks, mod, &res);
stoutf(&res.commit, commit, sizeof commit); stoutf(&res.commit, commit, sizeof commit);
stoutf(&res.preedit, preedit, sizeof preedit); stoutf(&res.preedit, preedit, sizeof preedit);
if(commit[0] != '\0') if(commit[0] != '\0')
emitcommit(c, dbus_message_get_path(m), commit); emitcommit(c, dbus_message_get_path(m), commit);
emitpreedit(c, dbus_message_get_path(m), preedit); emitpreedit(c, dbus_message_get_path(m), preedit);
r = dbus_message_new_method_return(m); return handlebool(c, m, res.eaten);
b = res.eaten ? TRUE : FALSE;
dbus_message_append_args(r, DBUS_TYPE_BOOLEAN, &b, DBUS_TYPE_INVALID);
dbus_connection_send(c, r, nil);
dbus_message_unref(r);
return DBUS_HANDLER_RESULT_HANDLED;
} }
static DBusHandlerResult static DBusHandlerResult
@@ -382,13 +505,65 @@ handlenoop(DBusConnection *c, DBusMessage *m)
} }
static DBusHandlerResult static DBusHandlerResult
handlereset(DBusConnection *c, DBusMessage *m) handlereset(DBusConnection *c, DBusMessage *m, Ictx *ctx)
{ {
sendreset(); Keyres res;
sendrequest(ctx, Keyreset, 0, 0, &res);
emitpreedit(c, dbus_message_get_path(m), ""); emitpreedit(c, dbus_message_get_path(m), "");
return handlenoop(c, m); return handlenoop(c, m);
} }
static DBusHandlerResult
handlefocusin(DBusConnection *c, DBusMessage *m, Ictx *ctx)
{
ctx->focused = 1;
return handlenoop(c, m);
}
static DBusHandlerResult
handlefocusout(DBusConnection *c, DBusMessage *m, Ictx *ctx)
{
releasecontext(ctx);
emitpreedit(c, dbus_message_get_path(m), "");
return handlenoop(c, m);
}
static DBusHandlerResult
handledestroy(DBusConnection *c, DBusMessage *m, Ictx *ctx)
{
dropcontext(ctx);
return handlenoop(c, m);
}
static DBusHandlerResult
handlecursor(DBusConnection *c, DBusMessage *m, Ictx *ctx)
{
DBusError err;
dbus_int32_t x, y, w, h;
Keyres res;
dbus_error_init(&err);
if(!dbus_message_get_args(m, &err,
DBUS_TYPE_INT32, &x, DBUS_TYPE_INT32, &y,
DBUS_TYPE_INT32, &w, DBUS_TYPE_INT32, &h,
DBUS_TYPE_INVALID)){
dbus_error_free(&err);
return handleerror(c, m, DBUS_ERROR_INVALID_ARGS,
"SetCursorLocation expects four integers");
}
if(w < 0 || h < 0)
return handleerror(c, m, DBUS_ERROR_INVALID_ARGS,
"cursor dimensions must not be negative");
ctx->caret.valid = 1;
ctx->caret.x = x;
ctx->caret.y = y;
ctx->caret.h = h;
if(ctx->focused)
sendrequest(ctx, Keycaret, 0, 0, &res);
return handlenoop(c, m);
}
static const char introspectxml[] = static const char introspectxml[] =
"<!DOCTYPE node PUBLIC \"-//freedesktop//DTD D-BUS Object Introspection 1.0//EN\"\n" "<!DOCTYPE node PUBLIC \"-//freedesktop//DTD D-BUS Object Introspection 1.0//EN\"\n"
" \"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd\">\n" " \"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd\">\n"
@@ -438,6 +613,7 @@ static DBusHandlerResult
onmsg(DBusConnection *c, DBusMessage *m, void *_) onmsg(DBusConnection *c, DBusMessage *m, void *_)
{ {
const char *iface, *member, *path; const char *iface, *member, *path;
Ictx *ctx;
USED(_); USED(_);
if(dbus_message_get_type(m) != DBUS_MESSAGE_TYPE_METHOD_CALL) if(dbus_message_get_type(m) != DBUS_MESSAGE_TYPE_METHOD_CALL)
@@ -462,15 +638,23 @@ onmsg(DBusConnection *c, DBusMessage *m, void *_)
return handlecreate(c, m); return handlecreate(c, m);
} }
if(strcmp(iface, "org.freedesktop.IBus.InputContext") == 0){ if(strcmp(iface, "org.freedesktop.IBus.InputContext") == 0){
ctx = findcontext(c, path);
if(ctx == nil)
return handleerror(c, m, DBUS_ERROR_UNKNOWN_OBJECT,
"unknown input context");
if(strcmp(member, "ProcessKeyEvent") == 0) if(strcmp(member, "ProcessKeyEvent") == 0)
return handlekey(c, m); return handlekey(c, m, ctx);
if(strcmp(member, "FocusOut") == 0 if(strcmp(member, "FocusIn") == 0)
|| strcmp(member, "Reset") == 0) return handlefocusin(c, m, ctx);
return handlereset(c, m); if(strcmp(member, "FocusOut") == 0)
if(strcmp(member, "FocusIn") == 0 return handlefocusout(c, m, ctx);
|| strcmp(member, "Destroy") == 0 if(strcmp(member, "Reset") == 0)
|| strcmp(member, "SetCapabilities") == 0 return handlereset(c, m, ctx);
|| strcmp(member, "SetCursorLocation") == 0 if(strcmp(member, "Destroy") == 0)
return handledestroy(c, m, ctx);
if(strcmp(member, "SetCursorLocation") == 0)
return handlecursor(c, m, ctx);
if(strcmp(member, "SetCapabilities") == 0
|| strcmp(member, "SetEngine") == 0) || strcmp(member, "SetEngine") == 0)
return handlenoop(c, m); return handlenoop(c, m);
} }
@@ -497,7 +681,7 @@ newconn(DBusServer *s, DBusConnection *c, void *_)
static void static void
pruneconns(void) pruneconns(void)
{ {
int i, j; int i, j, k;
j = 0; j = 0;
for(i = 0; i < nconns; i++){ for(i = 0; i < nconns; i++){
@@ -505,6 +689,9 @@ pruneconns(void)
conns[j++] = conns[i]; conns[j++] = conns[i];
continue; continue;
} }
for(k = 0; k < nelem(contexts); k++)
if(contexts[k].conn == conns[i])
dropcontext(&contexts[k]);
dbus_connection_unref(conns[i]); dbus_connection_unref(conns[i]);
} }
nconns = j; nconns = j;
@@ -521,8 +708,11 @@ ibusinit(void)
fprintf(stderr, "strans: ibus: cannot build address path\n"); fprintf(stderr, "strans: ibus: cannot build address path\n");
return -1; return -1;
} }
unlink(addrfile); if(snprintf(addr, sizeof(addr), "unix:abstract=strans-%d",
snprintf(addr, sizeof(addr), "unix:abstract=strans-%d", (int)getpid()); (int)getpid()) >= (int)sizeof addr){
fprintf(stderr, "strans: ibus: address is too long\n");
return -1;
}
dbus_error_init(&err); dbus_error_init(&err);
srv = dbus_server_listen(addr, &err); srv = dbus_server_listen(addr, &err);
if(srv == nil){ if(srv == nil){
@@ -552,6 +742,7 @@ void
ibusthread(void *_) ibusthread(void *_)
{ {
struct pollfd pfds[Maxwatches]; struct pollfd pfds[Maxwatches];
DBusWatch *polled[Maxwatches];
int wi[Maxwatches]; int wi[Maxwatches];
int i, n, rv; int i, n, rv;
unsigned int f; unsigned int f;
@@ -574,6 +765,7 @@ ibusthread(void *_)
if(f & DBUS_WATCH_READABLE) pfds[n].events |= POLLIN; if(f & DBUS_WATCH_READABLE) pfds[n].events |= POLLIN;
if(f & DBUS_WATCH_WRITABLE) pfds[n].events |= POLLOUT; if(f & DBUS_WATCH_WRITABLE) pfds[n].events |= POLLOUT;
wi[n] = i; wi[n] = i;
polled[n] = watches[i].w;
n++; n++;
} }
rv = poll(pfds, n, 200); rv = poll(pfds, n, 200);
@@ -582,14 +774,14 @@ ibusthread(void *_)
for(i = 0; i < n; i++){ for(i = 0; i < n; i++){
if(pfds[i].revents == 0) if(pfds[i].revents == 0)
continue; continue;
if(watches[wi[i]].w == nil) if(watches[wi[i]].w != polled[i])
continue; continue;
f = 0; f = 0;
if(pfds[i].revents & POLLIN) f |= DBUS_WATCH_READABLE; if(pfds[i].revents & POLLIN) f |= DBUS_WATCH_READABLE;
if(pfds[i].revents & POLLOUT) f |= DBUS_WATCH_WRITABLE; if(pfds[i].revents & POLLOUT) f |= DBUS_WATCH_WRITABLE;
if(pfds[i].revents & POLLHUP) f |= DBUS_WATCH_HANGUP; if(pfds[i].revents & POLLHUP) f |= DBUS_WATCH_HANGUP;
if(pfds[i].revents & POLLERR) f |= DBUS_WATCH_ERROR; if(pfds[i].revents & POLLERR) f |= DBUS_WATCH_ERROR;
dbus_watch_handle(watches[wi[i]].w, f); dbus_watch_handle(polled[i], f);
} }
for(i = 0; i < nconns; i++) for(i = 0; i < nconns; i++)
while(dbus_connection_dispatch(conns[i]) == DBUS_DISPATCH_DATA_REMAINS) while(dbus_connection_dispatch(conns[i]) == DBUS_DISPATCH_DATA_REMAINS)

104
ipc.c
View File

@@ -1,13 +1,12 @@
#include <errno.h> #include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h> #include <string.h>
#include <unistd.h> #include <unistd.h>
#include <sys/socket.h> #include <sys/socket.h>
#include <sys/un.h>
#include "ipc.h" #include "ipc.h"
#if !defined(MSG_NOSIGNAL) && !defined(SO_NOSIGPIPE)
#error "ipcsend requires MSG_NOSIGNAL or SO_NOSIGPIPE"
#endif
static void static void
putlen(unsigned char p[Ipclensz], size_t n) putlen(unsigned char p[Ipclensz], size_t n)
{ {
@@ -21,11 +20,54 @@ getlen(const unsigned char p[Ipclensz])
return p[0] | (p[1] << 8); return p[0] | (p[1] << 8);
} }
void int
ipcpackreq(unsigned char req[Ipcreqsz], int want, unsigned int mod, ipcpath(char *dst, size_t cap)
unsigned int key)
{ {
req[0] = want != 0; const char *dir, *sep;
int n;
if(dst == NULL || cap == 0)
return -1;
dir = getenv("XDG_RUNTIME_DIR");
if(dir != NULL && dir[0] == '/'){
sep = dir[strlen(dir)-1] == '/' ? "" : "/";
n = snprintf(dst, cap, "%s%sstrans.sock", dir, sep);
}else
n = snprintf(dst, cap, "/tmp/strans.%lu",
(unsigned long)getuid());
if(n < 0 || (size_t)n >= cap)
return -1;
return 0;
}
int
ipcconnect(void)
{
struct sockaddr_un addr;
int e, fd;
memset(&addr, 0, sizeof addr);
addr.sun_family = AF_UNIX;
if(ipcpath(addr.sun_path, sizeof addr.sun_path) < 0){
errno = ENAMETOOLONG;
return -1;
}
fd = socket(AF_UNIX, SOCK_STREAM, 0);
if(fd < 0)
return -1;
if(connect(fd, (struct sockaddr*)&addr, sizeof addr) == 0)
return fd;
e = errno;
close(fd);
errno = e;
return -1;
}
void
ipcpackreq(unsigned char req[Ipcreqsz], int want, uint32_t mod,
uint32_t key)
{
req[0] = want ? Ipcreqwant : 0;
req[1] = mod; req[1] = mod;
req[2] = key; req[2] = key;
req[3] = key >> 8; req[3] = key >> 8;
@@ -34,15 +76,28 @@ ipcpackreq(unsigned char req[Ipcreqsz], int want, unsigned int mod,
} }
void void
ipcunpackreq(const unsigned char req[Ipcreqsz], int *want, unsigned int *mod, ipcpackreset(unsigned char req[Ipcreqsz], int want)
unsigned int *key)
{ {
*want = req[0] != 0; memset(req, 0, Ipcreqsz);
req[0] = Ipcreqreset | (want ? Ipcreqwant : 0);
}
void
ipcunpackreq(const unsigned char req[Ipcreqsz], int *want, uint32_t *mod,
uint32_t *key)
{
*want = (req[0] & Ipcreqwant) != 0;
*mod = req[1] & Mmask; *mod = req[1] & Mmask;
*key = (unsigned int)req[2] | *key = (uint32_t)req[2] |
((unsigned int)req[3] << 8) | ((uint32_t)req[3] << 8) |
((unsigned int)req[4] << 16) | ((uint32_t)req[4] << 16) |
((unsigned int)req[5] << 24); ((uint32_t)req[5] << 24);
}
int
ipcreqreset(const unsigned char req[Ipcreqsz])
{
return (req[0] & Ipcreqreset) != 0;
} }
int int
@@ -97,27 +152,10 @@ ipcsend(int fd, const void *buf, size_t n)
{ {
const unsigned char *p; const unsigned char *p;
ssize_t r; ssize_t r;
int flags;
#if !defined(MSG_NOSIGNAL) && defined(SO_NOSIGPIPE)
int one, sr;
#endif
p = buf; p = buf;
#ifdef MSG_NOSIGNAL
flags = MSG_NOSIGNAL;
#else
flags = 0;
#ifdef SO_NOSIGPIPE
one = 1;
do
sr = setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof one);
while(sr < 0 && errno == EINTR);
if(sr < 0)
return -1;
#endif
#endif
while(n > 0){ while(n > 0){
r = send(fd, p, n, flags); r = send(fd, p, n, MSG_NOSIGNAL);
if(r < 0 && errno == EINTR) if(r < 0 && errno == EINTR)
continue; continue;
if(r <= 0) if(r <= 0)

24
ipc.h
View File

@@ -1,12 +1,11 @@
#ifndef STRANS_IPC_H
#define STRANS_IPC_H
#include <stddef.h> #include <stddef.h>
#include <stdint.h>
#define IPCPATH "/tmp/strans.%d"
/* /*
* Request: [want-preedit, modifiers, key byte 0, ..., key byte 3]. * Request: [flags, modifiers, key byte 0, ..., key byte 3].
* Flags request preedit and distinguish lifecycle reset from physical Escape.
* The server assigns the connection's owner generation; it is not sent on
* the wire.
* Only Mmask modifier bits are retained. * Only Mmask modifier bits are retained.
* Response: [eaten, commit-length-low, commit-length-high, commit...], * Response: [eaten, commit-length-low, commit-length-high, commit...],
* followed, when requested, by * followed, when requested, by
@@ -16,6 +15,9 @@
enum enum
{ {
Ipcreqwant = 1<<0,
Ipcreqreset = 1<<1,
Kspec = 0x110000, Kspec = 0x110000,
Kback = Kspec|0x08, Kback = Kspec|0x08,
Ktab = Kspec|0x09, Ktab = Kspec|0x09,
@@ -47,11 +49,13 @@ struct Ipcresp
size_t npreedit; size_t npreedit;
}; };
void ipcpackreq(unsigned char[Ipcreqsz], int, unsigned int, unsigned int); void ipcpackreq(unsigned char[Ipcreqsz], int, uint32_t, uint32_t);
void ipcunpackreq(const unsigned char[Ipcreqsz], int*, unsigned int*, unsigned int*); void ipcpackreset(unsigned char[Ipcreqsz], int);
void ipcunpackreq(const unsigned char[Ipcreqsz], int*, uint32_t*, uint32_t*);
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, const char*, size_t, const char*, size_t, int);
int ipcreadn(int, void*, size_t); int ipcreadn(int, void*, size_t);
int ipcsend(int, const void*, size_t); int ipcsend(int, const void*, size_t);
int ipcreadresp(int, int, char*, size_t, char*, size_t, Ipcresp*); int ipcreadresp(int, int, char*, size_t, char*, size_t, Ipcresp*);
int ipcpath(char*, size_t);
#endif int ipcconnect(void);

28
main.c
View File

@@ -7,12 +7,6 @@ Channel *dictreqc;
Channel *dictresc; Channel *dictresc;
char *fontdir; char *fontdir;
int
threadmaybackground(void)
{
return 1;
}
void void
usage(void) usage(void)
{ {
@@ -38,10 +32,9 @@ emalloc(ulong n)
{ {
void *p; void *p;
p = malloc(n); p = mallocz(n, 1);
if(p == nil) if(p == nil)
die("out of memory"); die("out of memory");
memset(p, 0, n);
return p; return p;
} }
@@ -67,12 +60,15 @@ threadmain(int argc, char **argv)
dictresc = chancreate(sizeof(Dictres), 0); dictresc = chancreate(sizeof(Dictres), 0);
mapinit(argv[1]); mapinit(argv[1]);
dictinit(argv[1]); dictinit(argv[1]);
proccreate(drawthread, nil, 16384); if(proccreate(drawthread, nil, 16384) < 0)
proccreate(srvthread, nil, 16384); die("can't create draw worker");
proccreate(ibusthread, nil, 32768); if(proccreate(srvthread, nil, 16384) < 0)
proccreate(waylandthread, nil, 32768); die("can't create server worker");
threadcreate(dictthread, nil, 16384); if(proccreate(ibusthread, nil, 32768) < 0)
threadcreate(imthread, nil, 16384); die("can't create IBus worker");
if(proccreate(waylandthread, nil, 32768) < 0)
threadexits(nil); die("can't create Wayland worker");
if(threadcreate(dictthread, nil, 16384) < 0)
die("can't create dictionary worker");
imthread(nil);
} }

View File

@@ -1,12 +1,41 @@
# # Japanese dictionary data
# The following are a set of tools to obtain and process dictionaries from the SKK project in order to use them with ktrans(1).
#
# grabskkdicts pulls the skk kana-kanji conversion dictionaries from the skk-dev/dict repo.
# skk2ktrans takes an skk dictionary and converts it into a kanji jisho suitable to be used with ktrans(1).
#
# You can fetch and convert all the dictionaries by running this file.
#
grabskkdicts `kanji.dict` is the historical dictionary bundled with strans. Its own header
for(d in skkdicts/SKK-JISYO.*) identifies it as the SKK Medium dictionary, version 8.1 of May 24, 1995,
<$d skk2ktrans >$d.jisho rearranged for Plan 9 ktrans by Kenji Okamoto on February 17, 2000. It was
already present when this repository was created in commit
`dcd1147638f908eca7e4b7616815e215022cc99d` on December 23, 2025. No original
SKK input file or upstream revision was committed, so the current file cannot
honestly be regenerated byte-for-byte from repository artifacts. The license
notice in its header permits redistribution and modification under GPL version
2 or later.
The repository copy keeps that historical candidate order. Duplicate keys
were merged at their first occurrence, later unseen candidates were appended
in source order, duplicate candidates were removed, and the empty
`ようたつ` row was removed.
## Importing a current SKK dictionary
From the repository root, fetch a known upstream revision and convert it with:
```
git clone https://github.com/skk-dev/dict.git map/skkdicts
git -C map/skkdicts checkout --detach REVISION
map/skk2ktrans map/skkdicts/SKK-JISYO.M >map/kanji.dict.new
python3 map/verifymap.py map/kanji.dict.new
```
Use a full skk-dev/dict commit ID for `REVISION` and record it when replacing
the bundled data. Git is needed only to fetch upstream data; it is not part of
the normal build image.
`skk2ktrans` accepts one or more EUC-JP SKK files (or standard input), writes
UTF-8 tab-separated rows, and merges input in command-line and source order.
It strips annotations, deduplicates candidates, and omits candidates containing
whitespace. Rows containing expressions or escaped candidate delimiters are
rejected; rewrite or omit those rows before import.
`verifymap.py` checks UTF-8, row structure, unique keys, 64-rune keys and
values, canonical candidate spacing, and duplicate dictionary candidates.
Pass it the exact `.map` and `.dict` files installed by the build.

View File

@@ -75,9 +75,14 @@ bl 😊
blu 😊 blu 😊
blus 😊 blus 😊
blush 😊 blush 😊
c χ c χ ☕️
ch χ ch χ
chi χ chi χ
co ☕️
cof ☕️
coff ☕️
coffe ☕️
coffee ☕️
d ° δ Δ ↓ d ° δ Δ ↓
de Δ ° δ de Δ ° δ
deg ° deg °
@@ -198,6 +203,65 @@ zet ζ
zeta ζ zeta ζ
~ ≈ ~ ≈
~= ≈ ~= ≈
あ ❤️
あい ❤️
い 👍
いい 👍
いいね 👍
え 😀 😄 😊
えが 😀 😄 😊
えがお 😀 😄 😊
こ ☕️
こー ☕️
こーひ ☕️
こーひー ☕️
さ 👍
さん 👍
さんせ 👍
さんせい 👍
ば 😂
ばく 😂
ばくし 😂
ばくしょ 😂
ばくしょう 😂
ほ 😄 😊
ほほ 😄 😊
ほほえ 😄 😊
ほほえみ 😄 😊
わ 😀 😂
わら 😀 😂
わらい 😀 😂
ア ❤️
アイ ❤️
イ 👍
イイ 👍
イイネ 👍
エ 😀 😄 😊
エガ 😀 😄 😊
エガオ 😀 😄 😊
コ ☕️
コー ☕️
コーヒ ☕️
コーヒー ☕️
サ 👍
サン 👍
サンセ 👍
サンセイ 👍
ハ ❤️
ハー ❤️
ハート ❤️
バ 😂
バク 😂
バクシ 😂
バクショ 😂
バクショウ 😂
ホ 😄 😊
ホホ 😄 😊
ホホエ 😄 😊
ホホエミ 😄 😊
ワ 😀 😂
ワラ 😀 😂
ワライ 😀 😂
따 👍 따 👍
따봉 👍 따봉 👍
미 😄 😊 미 😄 😊
@@ -220,6 +284,8 @@ zeta ζ
좋아요 👍 좋아요 👍
찬 👍 찬 👍
찬성 👍 찬성 👍
커 ☕️
커피 ☕️
폭 😂 폭 😂
폭소 😂 폭소 😂
하 ❤️ 하 ❤️

View File

@@ -103,9 +103,10 @@
ζ zeta ζ zeta
# Small multilingual emoji seed. Shared aliases intentionally produce choices. # Small multilingual emoji seed. Shared aliases intentionally produce choices.
😀 웃음 웃다 스마일 smile grin 😀 웃음 웃다 스마일 えがお エガオ わらい ワライ smile grin
😄 웃음 미소 스마일 smile happy 😄 웃음 미소 스마일 えがお エガオ ほほえみ ホホエミ smile happy
😂 웃음 폭소 웃겨 joy lol 😂 웃음 폭소 웃겨 ばくしょう バクショウ わらい ワライ joy lol
😊 미소 방긋 smile blush happy 😊 미소 방긋 ほほえみ ホホエミ えがお エガオ smile blush happy
❤️ 사랑 하트 heart love ❤️ 사랑 하트 あい アイ ハート heart love
👍 좋아요 찬성 따봉 엄지 thumbsup like 👍 좋아요 찬성 따봉 엄지 いいね イイネ さんせい サンセイ thumbsup like
☕️ 커피 こーひー コーヒー coffee

View File

View File

@@ -1,2 +0,0 @@
#!/bin/rc
git/clone https://github.com/skk-dev/dict skkdicts

View File

View File

@@ -1,28 +0,0 @@
 
,
. 。
< 《
> 》
/
?
;
:
\ 、
| ・
`
~ 〜
!
@
#
$ ¥
&
*
(
)
-
+
=
[ 「
] 」
{ 『
} 』

View File

@@ -60,7 +60,6 @@
あたらs 新 あたらs 新
あつi 熱 暑 厚 あつi 熱 暑 厚
あつk 厚 熱 暑 あつk 厚 熱 暑
あつk 厚 熱 暑
あつs 暑 あつs 暑
あつm 集 厚 あつm 集 厚
あつかe 扱 あつかe 扱
@@ -350,7 +349,6 @@
かわいr 可愛 かわいr 可愛
かんj 感 かんj 感
かんs 関 かんs 関
かんj 感
かんz 感 かんz 感
かんがe 考 かんがe 考
かんしゃs 感謝 かんしゃs 感謝
@@ -455,7 +453,7 @@
こu 乞 請 こu 乞 請
こy 来 こy 来
こうj 高 こうj 高
こうしょう 鉱床 高尚 こうしょう 鉱床 高尚 交渉
こうぶつ 鉱物 好物 こうぶつ 鉱物 好物
こうりょs 考慮 こうりょs 考慮
こえt 超 こえt 超
@@ -665,8 +663,6 @@
ただc 直 ただc 直
ただt 直 ただt 直
たとe 例 たとe 例
たおr 倒
たおr 倒
たのm 頼 たのm 頼
たのn 頼 たのn 頼
たのs 楽 たのs 楽
@@ -742,7 +738,6 @@
つみあげr 積み上げ つみあげr 積み上げ
つみあげt 積み上げ つみあげt 積み上げ
つめt 冷 つめt 冷
つづr 綴
つよi 強 つよi 強
つよk 強 つよk 強
つらi 辛 つらi 辛
@@ -793,7 +788,6 @@
とt 取 撮 採 とt 取 撮 採
とu 問 とu 問
とw 問 とw 問
とj 閉
といあw 問い合 といあw 問い合
とうj 投 とうj 投
とおi 遠 とおi 遠
@@ -913,13 +907,12 @@
ねらt 狙 ねらt 狙
ねらu 狙 ねらu 狙
のb 述 伸 延 のb 述 伸 延
のk 退 のk 退
のm 飲 呑 のm 飲 呑
のn 飲 のn 飲
のr 乗 載 のr 乗 載
のs 載 乗 のs 載 乗
のt 乗 載 のt 乗 載
のk 乗
のこr 残 のこr 残
のこs 残 のこs 残
のこt 残 のこt 残
@@ -1059,12 +1052,11 @@
ぼうえいs 防衛 ぼうえいs 防衛
まc 待 まc 待
まi 舞 まi 舞
まj 交 まj 交
まk 負 巻 まk 負 巻
まs 増 まs 増
まt 待 まt 待
まu 舞 まu 舞
まj 混
まz 混 先 まz 混 先
まいr 参 詣 まいr 参 詣
まいt 参 詣 まいt 参 詣
@@ -1241,8 +1233,6 @@
よわm 弱 よわm 弱
よわs 弱 よわs 弱
よわt 弱 よわt 弱
よろこb 喜 慶
よろこb 喜 慶
ろんj 論 ろんj 論
ろんz 論 ろんz 論
わk 分 湧 わk 分 湧
@@ -2267,7 +2257,6 @@
がくわり 学割 がくわり 学割
がけ 崖 がけ 崖
がし 樫 がし 樫
かせい 火星
がそ 画素 がそ 画素
がそかん 画素間 がそかん 画素間
がそごと 画素毎 がそごと 画素毎
@@ -2993,7 +2982,6 @@
こうしゃ 後者 公社 校舎 こうしゃ 後者 公社 校舎
こうしゅう 講習 公衆 こうしゅう 講習 公衆
こうしゅうかい 講習会 こうしゅうかい 講習会
こうしょう 交渉 鉱床
こうしん 更新 交信 後身 こうしん 更新 交信 後身
こうじ 麹 公示 高次 工事 孝二 こうじ 麹 公示 高次 工事 孝二
こうじつ 口実 こうじつ 口実
@@ -4802,7 +4790,7 @@
たいきゅうりょく 耐久力 たいきゅうりょく 耐久力
たいきょく 対極 たいきょく 対極
たいきん 大金 たいきん 大金
たいく 体躯 たいく 体躯 体育
たいくつ 退屈 たいくつ 退屈
たいけい 体系 体型 たいけい 体系 体型
たいけん 体験 たいけん 体験
@@ -4949,8 +4937,6 @@
たんらく 短絡 たんらく 短絡
だ 騨 駄 陀 楕 舵 柁 打 惰 妥 堕 唾 田 朶 だ 騨 駄 陀 楕 舵 柁 打 惰 妥 堕 唾 田 朶
だい 大 第 内 代 台 題 醍 だい 大 第 内 代 台 題 醍
たいく 体育
たいいく 体育
だいいち 第一 だいいち 第一
だいがく 大学 だいがく 大学
だいがくいん 大学院 だいがくいん 大学院
@@ -7314,7 +7300,6 @@
よこう 予行 予稿 よこう 予行 予稿
よこうち 横内 よこうち 横内
よこく 予告 よこく 予告
ようたつ
よこて 横手 よこて 横手
よこはま 横浜 よこはま 横浜
よこもじ 横文字 よこもじ 横文字
@@ -7426,7 +7411,7 @@
りゅうど 粒度 りゅうど 粒度
りゆう 理由 りゆう 理由
りょ 虜 旅 慮 侶 りょ 虜 旅 慮 侶
りょう 量 寮 両 領 陵 遼 諒 良 糧 稜 瞭 療 猟 涼 梁 料 凌 僚 亮 了 漁 粮 霊 りょう 量 寮 両 領 陵 遼 諒 良 糧 稜 瞭 療 猟 涼 梁 料 凌 僚 亮 了 漁 粮 霊
りょうあし 両足 りょうあし 両足
りょういき 領域 りょういき 領域
りょうか 量化 りょうか 量化

View File

@@ -33,7 +33,6 @@ sho ショ
syo ショ syo ショ
ja ジャ ja ジャ
ju ジュ ju ジュ
je ジェ
jo ジョ jo ジョ
su ス su ス
zu ズ zu ズ
@@ -119,6 +118,7 @@ xn ン
v ヴ v ヴ
xa ァ xa ァ
xi ィ xi ィ
xu ゥ
xe ェ xe ェ
xo ォ xo ォ
cc ッ cc ッ
@@ -128,7 +128,6 @@ pp ッ
tt ッ tt ッ
tch ッ tch ッ
ss ッ ss ッ
xn ン
di ディ di ディ
fa ファ fa ファ
fi フィ fi フィ
@@ -136,6 +135,7 @@ fe フェ
fo フォ fo フォ
va ヴァ va ヴァ
vi ヴィ vi ヴィ
vu ヴ
ve ヴェ ve ヴェ
vo ヴォ vo ヴォ
. 。 . 。

View File

@@ -38,7 +38,7 @@ def read(path):
for field in fields[1:]: for field in fields[1:]:
alias = fold(field) alias = fold(field)
if (not alias or len(alias) > MAXRUNES or hascontrol(alias) if (not alias or len(alias) > MAXRUNES or hascontrol(alias)
or alias != alias.strip()): or alias != alias.strip() or alias.startswith(";")):
raise ValueError(f"{path}:{lineno}: bad alias") raise ValueError(f"{path}:{lineno}: bad alias")
entries.append((result, alias)) entries.append((result, alias))
return entries return entries

File diff suppressed because it is too large Load Diff

View File

@@ -1,2 +1,63 @@
#!/bin/rc #!/bin/bash
tcs -sf ujis | awk '$1 !~ /;;/ {gsub("(^\/|\/$)", "", $2); gsub(" ", " "); gsub("\/", " ", $2);} {print}'
set -euo pipefail
transcode()
{
local file
if (( $# == 0 )); then
iconv -f EUC-JP -t UTF-8
return
fi
for file in "$@"; do
iconv -f EUC-JP -t UTF-8 "$file"
printf '\n'
done
}
transcode "$@" | awk '
function fail(s) {
print "skk2ktrans: " FILENAME ":" FNR ": " s >"/dev/stderr"
bad = 1
exit 1
}
function add(k, v, id) {
sub(/;.*/, "", v)
if(v == "" || v ~ /^[([#]/ || v ~ /[[:space:]]/)
return
id = k SUBSEP v
if(seen[id])
return
seen[id] = 1
if(!(k in row))
order[++nkey] = k
else
row[k] = row[k] " "
row[k] = row[k] v
}
/^;;/ || /^[[:space:]]*$/ {
next
}
{
if(!match($0, /[[:space:]]+/))
fail("missing candidate list")
key = substr($0, 1, RSTART-1)
field = substr($0, RSTART+RLENGTH)
if(key == "" || key ~ /^;/ || field !~ /^\/.*\/$/)
fail("invalid row")
if(field ~ /\\/)
fail("escaped candidates are unsupported")
if(field ~ /\/[([#]/)
fail("expression candidates are unsupported")
n = split(substr(field, 2, length(field)-2), value, "/")
for(i = 1; i <= n; i++)
add(key, value[i])
}
END {
if(bad)
exit 1
for(i = 1; i <= nkey; i++)
print order[i] "\t" row[order[i]]
}
'

73
map/verifymap.py Executable file
View File

@@ -0,0 +1,73 @@
#!/usr/bin/env python3
"""Verify the text maps and dictionaries consumed by strans."""
import argparse
import sys
from pathlib import Path
def verify(path):
errors = []
try:
data = path.read_bytes()
except OSError as error:
return [f"{path}: {error.strerror}"]
try:
text = data.decode("utf-8")
except UnicodeDecodeError as error:
return [f"{path}: invalid UTF-8: {error}"]
keys = {}
for lineno, line in enumerate(text.split("\n"), 1):
where = f"{path}:{lineno}"
if "\r" in line:
errors.append(f"{where}: carriage return is not canonical")
line = line.replace("\r", "")
if not line or line.startswith(";"):
continue
if "\0" in line:
errors.append(f"{where}: embedded NUL")
if line.count("\t") != 1:
errors.append(f"{where}: expected exactly one tab")
continue
key, value = line.split("\t")
if not key:
errors.append(f"{where}: empty key")
if len(key) > 64:
errors.append(f"{where}: key has {len(key)} runes; maximum is 64")
if key in keys:
errors.append(f"{where}: duplicate key; first defined on line {keys[key]}")
else:
keys[key] = lineno
if not value:
errors.append(f"{where}: empty value")
continue
if value != " ".join(value.split(" ")):
errors.append(f"{where}: noncanonical candidate spacing")
values = value.split(" ")
if path.name.endswith(".map"):
if len(value) > 64:
errors.append(f"{where}: value exceeds 64 runes")
else:
for candidate in values:
if len(candidate) > 64:
errors.append(f"{where}: candidate exceeds 64 runes")
if len(values) != len(set(values)):
errors.append(f"{where}: duplicate candidate")
return errors
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("file", nargs="+", type=Path)
args = parser.parse_args()
errors = []
for path in args.file:
errors.extend(verify(path))
for error in errors:
print(error, file=sys.stderr)
return bool(errors)
if __name__ == "__main__":
raise SystemExit(main())

File diff suppressed because it is too large Load Diff

33
popup_layout.c Normal file
View File

@@ -0,0 +1,33 @@
#include "dat.h"
#include "fn.h"
int
popupcells(Rune *r, int n)
{
int i, w;
w = 0;
for(i = 0; i < n; i++)
if(r[i] != 0xfe0e && r[i] != 0xfe0f)
w++;
return w;
}
void
popupposition(Caret *caret, int pointerx, int pointery, int screenw,
int screenh, int w, int h, int *x, int *y)
{
vlong px, py, xmax, ymax;
if(caret->valid){
px = caret->x;
py = (vlong)caret->y + max(caret->h, 0);
}else{
px = (vlong)pointerx + 10;
py = (vlong)pointery + 10;
}
xmax = max((vlong)screenw - w, 0);
ymax = max((vlong)screenh - h, 0);
*x = max(0, min(px, xmax));
*y = max(0, min(py, ymax));
}

45
srv.c
View File

@@ -1,9 +1,19 @@
#include "dat.h" #include "dat.h"
#include "fn.h" #include "fn.h"
static char adir[40]; #include <sys/stat.h>
#include <sys/un.h>
static char adir[256];
static Channel *clientc; static Channel *clientc;
typedef struct Client Client;
struct Client
{
int fd;
uvlong owner;
};
int int
srvreadkey(int fd, Keyreq *kr) srvreadkey(int fd, Keyreq *kr)
{ {
@@ -14,15 +24,18 @@ srvreadkey(int fd, Keyreq *kr)
if(ipcreadn(fd, req, sizeof req) < 0) if(ipcreadn(fd, req, sizeof req) < 0)
return -1; return -1;
ipcunpackreq(req, &want, &mod, &ks); ipcunpackreq(req, &want, &mod, &ks);
kr->op = ipcreqreset(req) ? Keyreset : Keypress;
kr->ks = ks; kr->ks = ks;
kr->mod = mod; kr->mod = mod;
kr->want = want; kr->want = want;
memset(&kr->caret, 0, sizeof kr->caret);
return 0; return 0;
} }
static void static void
clientthread(void *arg) clientthread(void *arg)
{ {
Client *client;
Channel *reply; Channel *reply;
int fd; int fd;
Keyreq kr; Keyreq kr;
@@ -32,10 +45,12 @@ clientthread(void *arg)
int n, ncommit, npreedit; int n, ncommit, npreedit;
uchar token; uchar token;
fd = (int)(uintptr)arg; client = arg;
fd = client->fd;
threadsetname("client %d", fd); threadsetname("client %d", fd);
reply = chancreate(sizeof(Keyres), 0); reply = chancreate(sizeof(Keyres), 0);
kr.reply = reply; kr.reply = reply;
kr.owner = client->owner;
while(srvreadkey(fd, &kr) == 0){ while(srvreadkey(fd, &kr) == 0){
chansend(keyc, &kr); chansend(keyc, &kr);
chanrecv(reply, &res); chanrecv(reply, &res);
@@ -46,25 +61,37 @@ clientthread(void *arg)
if(n < 0 || ipcsend(fd, out, n) < 0) if(n < 0 || ipcsend(fd, out, n) < 0)
break; break;
} }
kr.op = Keyrelease;
kr.want = 0;
chansend(keyc, &kr);
chanrecv(reply, &res);
chanfree(reply); chanfree(reply);
close(fd); close(fd);
free(client);
chanrecv(clientc, &token); chanrecv(clientc, &token);
} }
static void static void
srvinit(void) srvinit(void)
{ {
char addr[64]; char *addr, path[sizeof(((struct sockaddr_un*)0)->sun_path)];
snprint(addr, sizeof(addr), "unix!" IPCPATH, getuid()); if(ipcpath(path, sizeof path) < 0)
remove(addr + 5); die("IPC path is too long");
addr = smprint("unix!%s", path);
if(addr == nil)
die("out of memory");
if(announce(addr, adir) < 0) if(announce(addr, adir) < 0)
die("announce: %r"); die("IPC endpoint is already in use: %r");
free(addr);
if(chmod(path, 0600) < 0)
die("can't protect IPC endpoint: %s", path);
} }
void void
srvthread(void*) srvthread(void*)
{ {
Client *client;
char ldir[40]; char ldir[40];
int fd; int fd;
uchar token; uchar token;
@@ -81,9 +108,13 @@ srvthread(void*)
close(fd); close(fd);
continue; continue;
} }
if(proccreate(clientthread, (void*)(uintptr)fd, 8192) < 0){ client = emalloc(sizeof *client);
client->fd = fd;
client->owner = ownernew();
if(proccreate(clientthread, client, 8192) < 0){
chanrecv(clientc, &token); chanrecv(clientc, &token);
close(fd); close(fd);
free(client);
} }
} }
} }

5
str.c
View File

@@ -7,6 +7,8 @@ sinit(Str *s, char *src, int n)
int len; int len;
s->n = 0; s->n = 0;
if(n < 0 || (n > 0 && src == nil))
return;
while(n > 0 && s->n < Maxrunes){ while(n > 0 && s->n < Maxrunes){
if(!fullrune(src, n)) if(!fullrune(src, n))
break; break;
@@ -28,8 +30,9 @@ sclear(Str *s)
void void
sputr(Str *s, Rune r) sputr(Str *s, Rune r)
{ {
/* Str is a capped value; appends at capacity leave it unchanged. */
if(s->n >= Maxrunes) if(s->n >= Maxrunes)
die("sputr overflow"); return;
s->r[s->n++] = r; s->r[s->n++] = r;
} }

293
strans.c
View File

@@ -4,7 +4,14 @@
static Im im; static Im im;
static int popup = 0; static int popup = 0;
static int visible = 0; static int visible = 0;
static Lock ownerlock;
static uvlong ownerctr;
static uvlong activeowner;
static Caret caret;
static void dictqmap(Im*); static void dictqmap(Im*);
static void dictqjp(Im*);
static void backjp(Im*);
static int maplookup(Trie*, Str*, Str*);
typedef struct Search Search; typedef struct Search Search;
struct Search struct Search
@@ -15,17 +22,24 @@ struct Search
}; };
static Search search; static Search search;
static void uvlong
backmap(Im *im) ownernew(void)
{ {
spopr(&im->pre); uvlong n;
lock(&ownerlock);
n = ++ownerctr;
if(n == 0)
n = ++ownerctr;
unlock(&ownerlock);
return n;
} }
Lang langs[] = { Lang langs[] = {
{LangEN, "english", nil, transmap, backmap, dictqmap, nil, nil}, {LangEN, nil, nil, nil, nil, nil, nil, nil},
{LangJP, "hira", "kanji", transmap, backmap, dictqmap, nil, nil}, {LangJP, "hira", "kanji", transmap, backjp, dictqjp, nil, nil},
{LangJPK, "kata", "kanji", transmap, backmap, dictqmap, nil, nil}, {LangJPK, "kata", nil, transmap, backjp, dictqjp, nil, nil},
{LangKO, "hangul", nil, transko, backko, dictqmap, nil, nil}, {LangKO, nil, nil, transko, backko, nil, nil, nil},
{LangEMOJI, nil, "emoji", nil, nil, nil, nil, nil}, {LangEMOJI, nil, "emoji", nil, nil, nil, nil, nil},
{LangVI, "telex", nil, transvi, backvi, dictqmap, nil, nil}, {LangVI, "telex", nil, transvi, backvi, dictqmap, nil, nil},
}; };
@@ -38,6 +52,48 @@ clearkouho(void)
im.sel = -1; im.sel = -1;
} }
static int
isjp(Im *p)
{
return p->l->lang == LangJP || p->l->lang == LangJPK;
}
/*
* Japanese keeps completed kana in pre and an ambiguous romaji syllable in
* raw. A mapped raw suffix is part of the reading even while it remains
* pending (notably n, which may still grow into nya).
*/
static int
jpreading(Im *p, Str *s)
{
Str mapped;
*s = p->pre;
if(p->raw.n == 0)
return 1;
if(mapget(p->l->map, &p->raw, &mapped)){
sappend(s, &mapped);
return 1;
}
sappend(s, &p->raw);
return 0;
}
static int
haspre(Im *p)
{
return p->pre.n != 0 || (isjp(p) && p->raw.n != 0);
}
static void
impre(Str *s)
{
if(isjp(&im))
jpreading(&im, s);
else
*s = im.pre;
}
static void static void
setkouho(Dictres *res) setkouho(Dictres *res)
{ {
@@ -55,20 +111,25 @@ show(void)
{ {
static Drawcmd old; static Drawcmd old;
Drawcmd dc; Drawcmd dc;
Str *pre; Str pre;
int i, first, n; int i, first, n;
sclear(&dc.pre); sclear(&dc.pre);
pre = search.on ? &search.text : &im.pre; if(search.on)
pre = search.text;
else
impre(&pre);
if(popup){ if(popup){
if(search.on || im.l->map == nil || !mapget(im.l->map, pre, &dc.pre)) if(search.on || isjp(&im) || im.l->map == nil ||
dc.pre = *pre; !mapget(im.l->map, &pre, &dc.pre))
dc.pre = pre;
} }
first = im.sel >= Maxdisp ? im.sel - Maxdisp + 1 : 0; first = im.sel >= Maxdisp ? im.sel - Maxdisp + 1 : 0;
n = im.nkouho - first; n = im.nkouho - first;
if(n > Maxdisp) n = Maxdisp; if(n > Maxdisp) n = Maxdisp;
dc.nkouho = n; dc.nkouho = n;
dc.sel = im.sel >= 0 ? im.sel - first : -1; dc.sel = im.sel >= 0 ? im.sel - first : -1;
dc.caret = caret;
for(i = 0; i < n; i++) for(i = 0; i < n; i++)
dc.kouho[i] = im.kouho[first + i]; dc.kouho[i] = im.kouho[first + i];
if(dc.pre.n == 0 && n == 0 && !visible) if(dc.pre.n == 0 && n == 0 && !visible)
@@ -85,6 +146,10 @@ reset(void)
{ {
sclear(&im.pre); sclear(&im.pre);
sclear(&im.raw); sclear(&im.raw);
search.on = 0;
sclear(&search.raw);
sclear(&search.text);
memset(&caret, 0, sizeof caret);
clearkouho(); clearkouho();
show(); show();
} }
@@ -92,11 +157,17 @@ reset(void)
void void
dictsend(Im *im, Str *key) dictsend(Im *im, Str *key)
{ {
Dictreq req; Dictreq old, req;
req.key = *key; req.key = *key;
req.lang = im->l->lang; req.lang = im->l->lang;
if(isjp(im))
jpreading(im, &req.pre);
else
req.pre = im->pre; req.pre = im->pre;
/* imthread is the sole producer; discard every obsolete queued lookup. */
while(channbrecv(dictreqc, &old) > 0)
;
channbsend(dictreqc, &req); channbsend(dictreqc, &req);
} }
@@ -107,11 +178,25 @@ dictqmap(Im *im)
clearkouho(); clearkouho();
show(); show();
if(im->l->map == nil)
return;
if(!mapget(im->l->map, &im->pre, &dict)) if(!mapget(im->l->map, &im->pre, &dict))
return; return;
dictsend(im, &dict); dictsend(im, &dict);
} }
static void
dictqjp(Im *im)
{
Str reading;
clearkouho();
show();
if(im->l->dict == nil || !jpreading(im, &reading) || reading.n == 0)
return;
dictsend(im, &reading);
}
static int static int
setlang(int c) setlang(int c)
{ {
@@ -129,7 +214,10 @@ commit(Str *com)
{ {
Str val; Str val;
if(im.l->map != nil && mapget(im.l->map, &im.pre, &val)) if(isjp(&im)){
jpreading(&im, &val);
sappend(com, &val);
}else if(im.l->map != nil && mapget(im.l->map, &im.pre, &val))
sappend(com, &val); sappend(com, &val);
else else
sappend(com, &im.pre); sappend(com, &im.pre);
@@ -137,13 +225,81 @@ commit(Str *com)
sclear(&im.raw); sclear(&im.raw);
} }
static void
backjp(Im *im)
{
if(im->raw.n != 0)
spopr(&im->raw);
else
spopr(&im->pre);
}
static int
issmalltsu(Str *s)
{
return s->n == 1 && (s->r[0] == L'' || s->r[0] == L'');
}
static void
keeptail(Str *s)
{
int i;
for(i = 1; i < s->n; i++)
s->r[i-1] = s->r[i];
if(s->n > 0)
s->n--;
}
static int
dotransjp(Rune c, Str *com)
{
Str key, mapped;
if(c == '\'' && im.raw.n == 1 && im.raw.r[0] == 'n'){
if(mapget(im.l->map, &im.raw, &mapped))
sappend(&im.pre, &mapped);
sclear(&im.raw);
dictqjp(&im);
return 1;
}
key = im.raw;
if(key.n < Maxrunes)
sputr(&key, c);
sclear(&mapped);
if(maplookup(im.l->map, &key, &mapped)){
im.raw = key;
if(issmalltsu(&mapped)){
sappend(&im.pre, &mapped);
keeptail(&im.raw);
}
dictqjp(&im);
return 1;
}
if(im.raw.n != 0 && mapget(im.l->map, &im.raw, &mapped)){
sappend(&im.pre, &mapped);
sclear(&im.raw);
sclear(&key);
sputr(&key, c);
if(maplookup(im.l->map, &key, &mapped)){
im.raw = key;
dictqjp(&im);
return 1;
}
}
commit(com);
clearkouho();
return 0;
}
static int static int
dotrans(Rune c, Str *com) dotrans(Rune c, Str *com)
{ {
Emit e; Emit e;
Dictreq req;
Str mapped; Str mapped;
if(isjp(&im))
return dotransjp(c, com);
e = im.l->trans(&im, c); e = im.l->trans(&im, c);
if(im.l->lang == LangVI){ if(im.l->lang == LangVI){
if(e.s.n > 0 || !e.eat) if(e.s.n > 0 || !e.eat)
@@ -165,10 +321,7 @@ dotrans(Rune c, Str *com)
sclear(&im.pre); sclear(&im.pre);
sappend(&im.pre, &e.next); sappend(&im.pre, &e.next);
if(e.eat && e.dict.n > 0){ if(e.eat && e.dict.n > 0){
req.key = e.dict; dictsend(&im, &e.dict);
req.lang = im.l->lang;
req.pre = im.pre;
channbsend(dictreqc, &req);
} }
return e.eat; return e.eat;
} }
@@ -238,6 +391,16 @@ ctrlkey(Rune c)
return c; return c;
} }
static Rune
kokey(Rune c, u32int mod)
{
if(c >= 'A' && c <= 'Z')
c += 'a' - 'A';
if((mod & Mshift) && c >= 'a' && c <= 'z')
c -= 'a' - 'A';
return c;
}
static int static int
ismodkey(u32int ks) ismodkey(u32int ks)
{ {
@@ -347,7 +510,7 @@ endsearch(void)
static void static void
startsearch(Str *com) startsearch(Str *com)
{ {
if(im.pre.n > 0) if(haspre(&im))
commit(com); commit(com);
sclear(&im.pre); sclear(&im.pre);
sclear(&im.raw); sclear(&im.raw);
@@ -484,7 +647,7 @@ keystroke(u32int ks, u32int mod, Str *com)
off = 0; off = 0;
if(im.sel >= Maxdisp) if(im.sel >= Maxdisp)
off = im.sel - Maxdisp + 1; off = im.sel - Maxdisp + 1;
if(n >= 0 && n < Maxdisp && off + n < im.nkouho){ if(n >= 0 && n < Maxdisp && off + n < im.nkouho && mod == 0){
sappend(com, &im.kouho[off + n]); sappend(com, &im.kouho[off + n]);
reset(); reset();
return 1; return 1;
@@ -495,7 +658,7 @@ keystroke(u32int ks, u32int mod, Str *com)
reset(); reset();
return 1; return 1;
} }
if(im.pre.n > 0){ if(haspre(&im)){
commit(com); commit(com);
reset(); reset();
return 1; return 1;
@@ -503,18 +666,19 @@ keystroke(u32int ks, u32int mod, Str *com)
return 0; return 0;
} }
if(ks == Kback){ if(ks == Kback){
if(im.pre.n == 0) if(!haspre(&im))
return 0; return 0;
im.l->back(&im); im.l->back(&im);
if(im.pre.n == 0){ if(!haspre(&im)){
reset(); reset();
return 1; return 1;
} }
if(im.l->dictq != nil)
im.l->dictq(&im); im.l->dictq(&im);
return 1; return 1;
} }
if(ks == Kesc){ if(ks == Kesc){
if(im.pre.n == 0) if(!haspre(&im))
return 0; return 0;
reset(); reset();
return 1; return 1;
@@ -535,9 +699,14 @@ keystroke(u32int ks, u32int mod, Str *com)
show(); show();
return 1; return 1;
} }
if(getlang(c) != nil){
if(isjp(&im) && haspre(&im))
commit(com);
reset(); reset();
if(setlang(c)) setlang(c);
return 1; return 1;
}
reset();
return 0; return 0;
} }
if(ks == '0' && im.nkouho > 0){ if(ks == '0' && im.nkouho > 0){
@@ -546,13 +715,25 @@ keystroke(u32int ks, u32int mod, Str *com)
return 1; return 1;
} }
if(ks > 0x7f || ks == ' '){ if(ks > 0x7f || ks == ' '){
if(im.pre.n == 0) if(!haspre(&im))
return 0; return 0;
commit(com); commit(com);
sputr(com, ks);
reset(); reset();
if(com->n >= Maxrunes)
return 0;
sputr(com, ks);
return 1; return 1;
} }
if(im.l->trans == nil)
return 0;
if(im.pre.n >= Maxrunes ||
(isjp(&im) && im.pre.n + im.raw.n >= Maxrunes)){
commit(com);
reset();
return 0;
}
if(im.l->lang == LangKO)
ks = kokey(ks, mod);
n = dotrans(ks, com); n = dotrans(ks, com);
show(); show();
return n; return n;
@@ -564,9 +745,17 @@ init(void)
memset(&im, 0, sizeof(im)); memset(&im, 0, sizeof(im));
im.l = getlang(LangEN); im.l = getlang(LangEN);
memset(&search, 0, sizeof search); memset(&search, 0, sizeof search);
memset(&caret, 0, sizeof caret);
activeowner = 0;
visible = 0; visible = 0;
} }
static int
meaningful(Keyreq *kr)
{
return kr->ks != 0 && !ismodkey(kr->ks);
}
static void static void
imhandlekey(Keyreq *kr) imhandlekey(Keyreq *kr)
{ {
@@ -574,17 +763,54 @@ imhandlekey(Keyreq *kr)
sclear(&res.commit); sclear(&res.commit);
sclear(&res.preedit); sclear(&res.preedit);
res.eaten = 1;
switch(kr->op){
case Keyrelease:
if(kr->owner == activeowner){
reset();
activeowner = 0;
}
break;
case Keyreset:
if(kr->owner == activeowner)
reset();
break;
case Keycaret:
if(kr->owner == activeowner){
caret = kr->caret;
show();
}
break;
default:
res.eaten = 0;
if(meaningful(kr)){
if(kr->owner != activeowner){
reset();
activeowner = kr->owner;
caret = kr->caret;
}else if(kr->caret.valid)
caret = kr->caret;
res.eaten = keystroke(kr->ks, kr->mod, &res.commit); res.eaten = keystroke(kr->ks, kr->mod, &res.commit);
if(kr->want) }
res.preedit = search.on ? search.text : im.pre; break;
}
if(kr->want && kr->owner == activeowner){
if(search.on)
res.preedit = search.text;
else
impre(&res.preedit);
}
chansend(kr->reply, &res); chansend(kr->reply, &res);
} }
static void static void
dictresult(Dictres *res) dictresult(Dictres *res)
{ {
Str pre;
impre(&pre);
if(search.on || res->lang != im.l->lang || if(search.on || res->lang != im.l->lang ||
scmp(&res->key, &im.pre) != 0) scmp(&res->key, &pre) != 0)
return; return;
setkouho(res); setkouho(res);
show(); show();
@@ -634,13 +860,16 @@ mapget(Trie *t, Str *key, Str *out)
void void
mapinit(char *dir) mapinit(char *dir)
{ {
char path[1024]; char *path;
int i; int i;
for(i = 0; i < nelem(langs); i++){ for(i = 0; i < nelem(langs); i++){
if(langs[i].mapname == nil) if(langs[i].mapname == nil)
continue; continue;
snprint(path, sizeof(path), "%s/%s.map", dir, langs[i].mapname); path = smprint("%s/%s.map", dir, langs[i].mapname);
if(path == nil)
die("out of memory");
langs[i].map = trieopen(path); langs[i].map = trieopen(path);
free(path);
} }
} }

View File

@@ -5,9 +5,11 @@ LIBS = -lthread -lbio
PROG = unit_test PROG = unit_test
TESTSRC = unit_test.c test_util.c str_test.c hash_test.c trie_test.c \ TESTSRC = unit_test.c test_util.c str_test.c hash_test.c trie_test.c \
ko_test.c vi_test.c engine_test.c dict_test.c ipc_test.c ko_test.c vi_test.c engine_test.c dict_test.c ipc_test.c wayland_test.c \
popup_test.c
TESTOBJ = $(TESTSRC:.c=.o) TESTOBJ = $(TESTSRC:.c=.o)
PARENTSRC = str.c hash.c trie.c dict.c ko.c vi.c ipc.c PARENTSRC = str.c hash.c trie.c dict.c ko.c vi.c ipc.c wayland_state.c \
popup_layout.c
PARENTOBJ = $(PARENTSRC:%.c=unit_%.o) PARENTOBJ = $(PARENTSRC:%.c=unit_%.o)
OBJS = $(TESTOBJ) $(PARENTOBJ) OBJS = $(TESTOBJ) $(PARENTOBJ)

View File

@@ -0,0 +1,3 @@
かんじ 漢字 幹事 感じ
えがお 笑顔
きごう 記号 普通
1 かんじ 漢字 幹事 感じ
2 えがお 笑顔
3 きごう 記号 普通

View File

@@ -0,0 +1,6 @@
;; converter fixture; the test transcodes this file to EUC-JP
かんじ /漢字;common/幹事/
えがお /笑顔;face/
かんじ /感じ/漢字;duplicate/
きごう /記号;symbol/普通/
むこう /候補 with space/

View File

@@ -3,4 +3,3 @@ ab beta
한 값 한 값
duplicate first duplicate first
duplicate second duplicate second
tabs one two

View File

@@ -89,3 +89,28 @@ dictionary_misses_clear_result(struct ct *t)
hmapfree(lang->dict); hmapfree(lang->dict);
lang->dict = saved; lang->dict = saved;
} }
void
dictionary_emoji_identity(struct ct *t)
{
Dictreq req;
Dictres res;
Hmap *saved;
Lang *lang;
Str key;
lang = getlang(LangEMOJI);
saved = lang->dict;
lang->dict = hmapalloc(2);
key = mkstr("é");
hmapset(&lang->dict, &key, "é", strlen("é"));
memset(&req, 0, sizeof req);
req.key = key;
req.pre = key;
req.lang = LangEMOJI;
dictlookup(&req, &res);
if(CT_EQ_INT(t, 1, res.nkouho))
checkstr(t, "emoji identity candidate", "é", &res.kouho[0]);
hmapfree(lang->dict);
lang->dict = saved;
}

View File

@@ -1,6 +1,13 @@
#define STRANS_TEST_ENGINE
#include "../strans.c" #include "../strans.c"
#include "test.h" #include "../cutest/cutest.h"
extern Lang testvi;
Str mkstr(char*);
int checkstr(struct ct*, char*, char*, Str*);
Str shownpre(Im*);
static int typekeys(struct ct*, char*, Str*);
void void
vietnamese_state_lifetime(struct ct *t) vietnamese_state_lifetime(struct ct *t)
@@ -73,7 +80,7 @@ engine_backspace_clears_candidates(struct ct *t)
im.sel = 0; im.sel = 0;
CT_CHECK(t, keystroke(Kback, 0, &com)); CT_CHECK(t, keystroke(Kback, 0, &com));
checkstr(t, "backspace commit", "", &com); checkstr(t, "backspace commit", "", &com);
checkstr(t, "raw preedit after backspace", "n", &im.pre); checkstr(t, "pending romaji after backspace", "n", &im.raw);
shown = shownpre(&im); shown = shownpre(&im);
checkstr(t, "shown preedit after backspace", "", &shown); checkstr(t, "shown preedit after backspace", "", &shown);
CT_EQ_INT(t, 0, im.nkouho); CT_EQ_INT(t, 0, im.nkouho);
@@ -106,6 +113,159 @@ engine_selects_visible_candidate(struct ct *t)
} }
} }
void
engine_candidate_shortcut_modifiers(struct ct *t)
{
static u32int mods[] = { Mctrl, Malt, Msuper };
Str com;
int i;
for(i = 0; i < nelem(mods); i++){
init();
im.kouho[0] = mkstr("must-not-select");
im.nkouho = 1;
im.sel = -1;
sclear(&com);
CT_CHECK(t, !keystroke('1', mods[i], &com));
CT_CHECK(t, scmp(&com, &im.kouho[0]) != 0);
}
}
void
engine_dictionary_queue_latest_wins(struct ct *t)
{
Channel *saved;
Dictreq old, got;
Str key;
saved = dictreqc;
dictreqc = chancreate(sizeof(Dictreq), 1);
memset(&old, 0, sizeof old);
old.key = mkstr("old-key");
old.pre = mkstr("old-preedit");
old.lang = LangJP;
CT_CHECK(t, channbsend(dictreqc, &old) > 0);
init();
im.l = getlang(LangJP);
im.pre = mkstr("new-preedit");
key = mkstr("new-key");
dictsend(&im, &key);
if(CT_CHECK(t, channbrecv(dictreqc, &got) > 0)){
checkstr(t, "newest dictionary key", "new-key", &got.key);
checkstr(t, "newest dictionary preedit", "new-preedit", &got.pre);
CT_EQ_INT(t, LangJP, got.lang);
}
CT_CHECK(t, channbrecv(dictreqc, &got) <= 0);
chanfree(dictreqc);
dictreqc = saved;
}
static Keyres
ownerrequestat(uvlong owner, int op, Rune key, u32int mod, Caret *caret)
{
Channel *reply;
Keyreq req;
Keyres res;
reply = chancreate(sizeof(Keyres), 1);
memset(&req, 0, sizeof req);
req.owner = owner;
req.op = op;
req.ks = key;
req.mod = mod;
if(caret != nil)
req.caret = *caret;
req.want = 1;
req.reply = reply;
imhandlekey(&req);
chanrecv(reply, &res);
chanfree(reply);
return res;
}
static Keyres
ownerrequest(uvlong owner, int op, Rune key, u32int mod)
{
return ownerrequestat(owner, op, key, mod, nil);
}
void
engine_active_owner_lifecycle(struct ct *t)
{
Keyres res;
Str shown;
uvlong a, b, c;
a = ownernew();
b = ownernew();
c = ownernew();
CT_CHECK(t, a != 0 && b != 0 && c != 0);
CT_CHECK(t, a != b && b != c && a != c);
init();
im.l = getlang(LangJP);
res = ownerrequest(a, Keypress, 'k', 0);
CT_CHECK(t, res.eaten);
res = ownerrequest(a, Keypress, 'a', 0);
checkstr(t, "first owner preedit", "", &res.preedit);
CT_EQ_UINT(t, a, activeowner);
res = ownerrequest(b, Keypress, 'n', 0);
checkstr(t, "takeover resets old preedit", "", &res.preedit);
CT_EQ_UINT(t, b, activeowner);
res = ownerrequest(a, Keyreset, 0, 0);
CT_EQ_INT(t, 0, res.preedit.n);
shown = shownpre(&im);
checkstr(t, "stale reset preserves owner", "", &shown);
CT_EQ_UINT(t, b, activeowner);
res = ownerrequest(b, Keypress, 'y', 0);
res = ownerrequest(b, Keypress, 'a', 0);
checkstr(t, "current owner continues", "にゃ", &res.preedit);
ownerrequest(a, Keyrelease, 0, 0);
shown = shownpre(&im);
checkstr(t, "stale release preserves owner", "にゃ", &shown);
CT_EQ_UINT(t, b, activeowner);
ownerrequest(b, Keyrelease, 0, 0);
CT_EQ_UINT(t, 0, activeowner);
CT_EQ_INT(t, 0, im.pre.n);
CT_EQ_INT(t, 0, im.raw.n);
ownerrequest(c, Keypress, Kmodfirst, 0);
CT_EQ_UINT(t, 0, activeowner);
res = ownerrequest(c, Keypress, 'k', 0);
CT_EQ_UINT(t, c, activeowner);
checkstr(t, "next owner first key", "k", &res.preedit);
}
void
engine_active_owner_caret(struct ct *t)
{
Caret a, moved;
uvlong one, two;
one = ownernew();
two = ownernew();
memset(&a, 0, sizeof a);
a.valid = 1;
a.x = 10;
a.y = 20;
a.h = 14;
moved = a;
moved.x = 80;
init();
im.l = getlang(LangJP);
ownerrequestat(one, Keypress, 'k', 0, &a);
CT_CHECK(t, caret.valid);
CT_EQ_INT(t, 10, caret.x);
ownerrequestat(two, Keycaret, 0, 0, &moved);
CT_EQ_INT(t, 10, caret.x);
ownerrequestat(one, Keycaret, 0, 0, &moved);
CT_EQ_INT(t, 80, caret.x);
ownerrequest(two, Keypress, 'n', 0);
CT_EQ_UINT(t, two, activeowner);
CT_CHECK(t, !caret.valid);
}
void void
engine_commit_contract(struct ct *t) engine_commit_contract(struct ct *t)
{ {
@@ -113,10 +273,11 @@ engine_commit_contract(struct ct *t)
init(); init();
im.l = getlang(LangJP); im.l = getlang(LangJP);
im.pre = mkstr("ka");
im.nkouho = 1; im.nkouho = 1;
im.sel = -1; im.sel = -1;
sclear(&com); sclear(&com);
if(!typekeys(t, "ka", &com))
return;
CT_CHECK(t, keystroke(Kret, 0, &com)); CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "return commit", "", &com); checkstr(t, "return commit", "", &com);
CT_EQ_INT(t, 0, im.pre.n); CT_EQ_INT(t, 0, im.pre.n);
@@ -126,12 +287,13 @@ engine_commit_contract(struct ct *t)
init(); init();
im.l = getlang(LangJP); im.l = getlang(LangJP);
im.pre = mkstr("ka"); sclear(&com);
if(!typekeys(t, "ka", &com))
return;
im.kouho[0] = mkstr("c1"); im.kouho[0] = mkstr("c1");
im.kouho[1] = mkstr("c2"); im.kouho[1] = mkstr("c2");
im.nkouho = 2; im.nkouho = 2;
im.sel = 1; im.sel = 1;
sclear(&com);
CT_CHECK(t, keystroke(Kret, 0, &com)); CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "selected candidate", "c2", &com); checkstr(t, "selected candidate", "c2", &com);
CT_EQ_INT(t, 0, im.pre.n); CT_EQ_INT(t, 0, im.pre.n);
@@ -140,10 +302,11 @@ engine_commit_contract(struct ct *t)
init(); init();
im.l = getlang(LangJP); im.l = getlang(LangJP);
im.pre = mkstr("ka"); sclear(&com);
if(!typekeys(t, "ka", &com))
return;
im.nkouho = 1; im.nkouho = 1;
im.sel = -1; im.sel = -1;
sclear(&com);
CT_CHECK(t, !keystroke('q', 0, &com)); CT_CHECK(t, !keystroke('q', 0, &com));
checkstr(t, "uneaten-key commit", "", &com); checkstr(t, "uneaten-key commit", "", &com);
CT_EQ_INT(t, 0, im.pre.n); CT_EQ_INT(t, 0, im.pre.n);
@@ -153,8 +316,9 @@ engine_commit_contract(struct ct *t)
init(); init();
im.l = getlang(LangJP); im.l = getlang(LangJP);
im.pre = mkstr("ka");
sclear(&com); sclear(&com);
if(!typekeys(t, "ka", &com))
return;
CT_CHECK(t, keystroke(0xf008, 0, &com)); CT_CHECK(t, keystroke(0xf008, 0, &com));
CT_EQ_INT(t, 2, com.n); CT_EQ_INT(t, 2, com.n);
CT_EQ_UINT(t, 0x304b, com.r[0]); CT_EQ_UINT(t, 0x304b, com.r[0]);
@@ -183,6 +347,95 @@ engine_telex_history_bound(struct ct *t)
CT_EQ_INT(t, 0, im.raw.n); CT_EQ_INT(t, 0, im.raw.n);
} }
void
engine_korean_modifiers_and_backspace(struct ct *t)
{
static const struct {
Rune key;
char *plain;
char *shifted;
} cases[] = {
{ 'r', "", "" }, { 'e', "", "" },
{ 'q', "", "" }, { 't', "", "" },
{ 'w', "", "" }, { 'o', "", "" },
{ 'p', "", "" },
};
Str com;
int i;
for(i = 0; i < nelem(cases); i++){
init();
im.l = getlang(LangKO);
sclear(&com);
CT_CHECK(t, keystroke(cases[i].key, 0, &com));
checkstr(t, "plain Korean key", cases[i].plain, &im.pre);
init();
im.l = getlang(LangKO);
sclear(&com);
CT_CHECK(t, keystroke(cases[i].key, Mshift, &com));
checkstr(t, "shifted Korean key", cases[i].shifted, &im.pre);
init();
im.l = getlang(LangKO);
sclear(&com);
CT_CHECK(t, keystroke(cases[i].key - 'a' + 'A', 0, &com));
checkstr(t, "Caps Lock Korean key", cases[i].plain, &im.pre);
init();
im.l = getlang(LangKO);
sclear(&com);
CT_CHECK(t, keystroke(cases[i].key, Mshift, &com));
checkstr(t, "Shift+Caps Korean key", cases[i].shifted, &im.pre);
}
init();
im.l = getlang(LangKO);
sclear(&com);
if(typekeys(t, "rkr", &com)){
checkstr(t, "Korean engine preedit", "", &im.pre);
CT_CHECK(t, keystroke(Kback, 0, &com));
checkstr(t, "Korean final backspace", "", &im.pre);
CT_CHECK(t, keystroke(Kback, 0, &com));
checkstr(t, "Korean vowel backspace", "", &im.pre);
CT_CHECK(t, keystroke(Kback, 0, &com));
CT_EQ_INT(t, 0, im.pre.n);
CT_CHECK(t, !keystroke(Kback, 0, &com));
}
}
void
engine_direct_language_modes(struct ct *t)
{
Str com;
init();
sclear(&com);
CT_EQ_INT(t, LangEN, im.l->lang);
CT_EQ_PTR(t, nil, im.l->mapname);
CT_EQ_PTR(t, nil, im.l->map);
CT_EQ_PTR(t, nil, im.l->trans);
CT_CHECK(t, !keystroke('a', 0, &com));
CT_CHECK(t, !keystroke('Z', Mshift, &com));
checkstr(t, "English direct commit", "", &com);
CT_EQ_INT(t, 0, im.pre.n);
CT_EQ_INT(t, 0, im.raw.n);
CT_CHECK(t, keystroke('e', Mctrl, &com));
CT_CHECK(t, search.on);
CT_EQ_INT(t, LangEN, im.l->lang);
CT_CHECK(t, keystroke(Kesc, 0, &com));
CT_CHECK(t, !search.on);
CT_CHECK(t, keystroke('s', Mctrl, &com));
CT_EQ_INT(t, LangKO, im.l->lang);
CT_EQ_PTR(t, nil, im.l->mapname);
CT_EQ_PTR(t, nil, im.l->map);
CT_EQ_PTR(t, nil, im.l->dictq);
CT_CHECK(t, keystroke('r', 0, &com));
checkstr(t, "Korean without map", "", &im.pre);
CT_CHECK(t, keystroke(Kback, 0, &com));
CT_EQ_INT(t, 0, im.pre.n);
}
void void
engine_language_switch_state(struct ct *t) engine_language_switch_state(struct ct *t)
{ {
@@ -309,6 +562,10 @@ emojibegin(Emojifix *f, int showpre)
setemoji(&l->dict, "smil", "😀 😄"); setemoji(&l->dict, "smil", "😀 😄");
setemoji(&l->dict, "smile", "😀 😄"); setemoji(&l->dict, "smile", "😀 😄");
setemoji(&l->dict, "웃음", "😀 😄 😂"); setemoji(&l->dict, "웃음", "😀 😄 😂");
setemoji(&l->dict, "えがお", "😀 😄 😊");
setemoji(&l->dict, "エガオ", "😀 😄 😊");
setemoji(&l->dict, "heart", "❤️");
setemoji(&l->dict, "coffee", "☕️");
setemoji(&l->dict, "^", "¹ ² ³ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹ ⁽"); setemoji(&l->dict, "^", "¹ ² ³ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹ ⁽");
setemoji(&l->dict, "^0", ""); setemoji(&l->dict, "^0", "");
setemoji(&l->dict, "_", "₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉ ₍"); setemoji(&l->dict, "_", "₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉ ₍");
@@ -346,6 +603,170 @@ typekeys(struct ct *t, char *keys, Str *com)
return 1; return 1;
} }
void
engine_japanese_readings(struct ct *t)
{
static const struct {
char *keys;
char *want;
} cases[] = {
{ "nya", "にゃ" },
{ "n'ya", "んや" },
{ "nnya", "んにゃ" },
{ "kin'youbi", "きんようび" },
};
Str com, shown;
int i;
for(i = 0; i < nelem(cases); i++){
init();
im.l = getlang(LangJP);
sclear(&com);
if(!typekeys(t, cases[i].keys, &com))
continue;
checkstr(t, cases[i].keys, "", &com);
shown = shownpre(&im);
checkstr(t, cases[i].keys, cases[i].want, &shown);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "committed reading", cases[i].want, &com);
}
}
static int
newestrequest(Dictreq *req)
{
int n;
n = 0;
while(channbrecv(dictreqc, req) > 0)
n++;
return n;
}
void
engine_japanese_candidates(struct ct *t)
{
Dictreq req;
Dictres res;
Hmap *saved;
Lang *jp;
Str com, key, shown;
jp = getlang(LangJP);
saved = jp->dict;
jp->dict = hmapalloc(8);
key = mkstr("かんじ");
hmapset(&jp->dict, &key, "漢字 幹事", strlen("漢字 幹事"));
init();
im.l = jp;
sclear(&com);
if(!typekeys(t, "kanji", &com))
goto cleanup;
shown = shownpre(&im);
checkstr(t, "complete Japanese reading", "かんじ", &shown);
if(!CT_CHECK(t, newestrequest(&req) > 0))
goto cleanup;
checkstr(t, "dictionary lookup key", "かんじ", &req.key);
checkstr(t, "dictionary request identity", "かんじ", &req.pre);
dictlookup(&req, &res);
dictresult(&res);
if(!CT_EQ_INT(t, 2, im.nkouho))
goto cleanup;
checkstr(t, "candidate one", "漢字", &im.kouho[0]);
checkstr(t, "candidate two", "幹事", &im.kouho[1]);
CT_CHECK(t, keystroke(Kdown, 0, &com));
CT_EQ_INT(t, 0, im.sel);
CT_CHECK(t, keystroke(Kdown, 0, &com));
CT_EQ_INT(t, 1, im.sel);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "selected Kanji", "幹事", &com);
cleanup:
newestrequest(&req);
hmapfree(jp->dict);
jp->dict = saved;
}
void
engine_japanese_backspace_and_boundaries(struct ct *t)
{
Str com, shown;
init();
im.l = getlang(LangJP);
sclear(&com);
if(typekeys(t, "kanji", &com)){
CT_CHECK(t, keystroke(Kback, 0, &com));
shown = shownpre(&im);
checkstr(t, "backspace pending i", "かんj", &shown);
CT_CHECK(t, keystroke(Kback, 0, &com));
shown = shownpre(&im);
checkstr(t, "backspace pending j", "かん", &shown);
CT_CHECK(t, keystroke(Kback, 0, &com));
shown = shownpre(&im);
checkstr(t, "backspace accumulated kana", "", &shown);
}
init();
im.l = getlang(LangJP);
sclear(&com);
if(typekeys(t, "kanji", &com)){
CT_CHECK(t, keystroke('k', Mctrl, &com));
checkstr(t, "Hiragana mode boundary", "かんじ", &com);
CT_EQ_INT(t, LangJPK, im.l->lang);
CT_EQ_INT(t, 0, im.pre.n);
CT_EQ_INT(t, 0, im.raw.n);
sclear(&com);
if(typekeys(t, "ka", &com)){
CT_CHECK(t, keystroke('n', Mctrl, &com));
checkstr(t, "Katakana mode boundary", "", &com);
CT_EQ_INT(t, LangJP, im.l->lang);
}
}
init();
im.l = getlang(LangJP);
sclear(&com);
if(typekeys(t, "kanji", &com)){
CT_CHECK(t, keystroke(' ', 0, &com));
checkstr(t, "real input boundary", "かんじ ", &com);
}
}
void
engine_katakana_sequences(struct ct *t)
{
static const struct {
char *keys;
char *want;
} cases[] = {
{ "kitte", "キッテ" },
{ "katta", "カッタ" },
{ "gakkou", "ガッコウ" },
{ "matcha", "マッチャ" },
{ "xu", "" },
{ "vu", "" },
};
Dictreq req;
Str com, shown;
int i;
newestrequest(&req);
for(i = 0; i < nelem(cases); i++){
init();
im.l = getlang(LangJPK);
sclear(&com);
if(!typekeys(t, cases[i].keys, &com))
continue;
checkstr(t, cases[i].keys, "", &com);
shown = shownpre(&im);
checkstr(t, cases[i].keys, cases[i].want, &shown);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "committed Katakana", cases[i].want, &com);
}
CT_EQ_INT(t, 0, newestrequest(&req));
CT_EQ_PTR(t, nil, getlang(LangJPK)->dictname);
}
void void
engine_emoji_single_candidate(struct ct *t) engine_emoji_single_candidate(struct ct *t)
{ {
@@ -439,6 +860,53 @@ engine_emoji_queries(struct ct *t)
emojiend(&f); emojiend(&f);
} }
void
engine_emoji_japanese_and_multirune(struct ct *t)
{
Emojifix f;
Str com;
emojibegin(&f, 0);
init();
im.l = getlang(LangJP);
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "egao", &com)){
checkstr(t, "localized Hiragana query", "えがお", &search.text);
CT_EQ_INT(t, 3, im.nkouho);
CT_CHECK(t, keystroke(Kesc, 0, &com));
}
init();
im.l = getlang(LangJPK);
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "egao", &com)){
checkstr(t, "localized Katakana query", "エガオ", &search.text);
CT_EQ_INT(t, 3, im.nkouho);
CT_CHECK(t, keystroke(Kesc, 0, &com));
}
init();
im.l = getlang(LangEN);
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "heart", &com)){
CT_EQ_INT(t, 1, im.nkouho);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "variation-selector emoji", "❤️", &com);
}
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "coffee", &com)){
CT_EQ_INT(t, 1, im.nkouho);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "multi-codepoint emoji", "☕️", &com);
}
emojiend(&f);
}
void void
engine_emoji_digit_aliases(struct ct *t) engine_emoji_digit_aliases(struct ct *t)
{ {
@@ -556,7 +1024,8 @@ engine_emoji_preedit_languages(struct ct *t)
sclear(&com); sclear(&com);
if(typekeys(t, "ka", &com)){ if(typekeys(t, "ka", &com)){
CT_CHECK(t, !keystroke(Kmodfirst+2, 0, &com)); CT_CHECK(t, !keystroke(Kmodfirst+2, 0, &com));
checkstr(t, "modifier preserves Japanese", "ka", &im.pre); shown = shownpre(&im);
checkstr(t, "modifier preserves Japanese", "", &shown);
CT_CHECK(t, keystroke('E', Mctrl|Mshift, &com)); CT_CHECK(t, keystroke('E', Mctrl|Mshift, &com));
checkstr(t, "committed Japanese preedit", "", &com); checkstr(t, "committed Japanese preedit", "", &com);
CT_CHECK(t, search.on); CT_CHECK(t, search.on);
@@ -701,3 +1170,146 @@ engine_emoji_dictionary_identity(struct ct *t)
checkstr(t, "matching response accepted", "right", &im.kouho[0]); checkstr(t, "matching response accepted", "right", &im.kouho[0]);
emojiend(&f); emojiend(&f);
} }
static u32int
stressrand(u32int *state)
{
*state = *state * 1664525U + 1013904223U;
return *state;
}
static int
stressstr(Str *s)
{
return s->n >= 0 && s->n <= Maxrunes;
}
void
engine_randomized_stress(struct ct *t)
{
static Rune text[] = {
'a', 'e', 'g', 'i', 'j', 'k', 'm', 'n', 'o', 'r', 's', 't',
'u', 'w', 'y', '1', '2', '9', '\'', ' ', L'',
};
static Rune modes[] = { 't', 'n', 'k', 's', 'v' };
Emojifix f;
Lang *vi;
Trie *vimap;
Keyres res;
Caret pos;
u32int rnd, mod;
uvlong owners[3], owner;
int i, j, op;
vi = getlang(LangVI);
if(!CT_CHECK(t, vi != nil))
return;
vimap = vi->map;
vi->map = testvi.map;
emojibegin(&f, 0);
owners[0] = ownernew();
owners[1] = ownernew();
owners[2] = ownernew();
rnd = 0x5eed1234U;
for(i = 0; i < 16000; i++){
rnd = stressrand(&rnd);
owner = owners[(rnd >> 8) % nelem(owners)];
op = rnd % 28;
mod = (rnd >> 16) & Mmask;
memset(&pos, 0, sizeof pos);
pos.valid = (rnd >> 25) & 1;
pos.x = (rnd >> 3) & 0x3ff;
pos.y = (rnd >> 13) & 0x3ff;
pos.h = 1 + ((rnd >> 18) & 0x1f);
if(op < 13)
res = ownerrequestat(owner, Keypress,
text[(rnd >> 5) % nelem(text)], mod, &pos);
else if(op == 13)
res = ownerrequestat(owner, Keypress, Kback, 0, &pos);
else if(op == 14)
res = ownerrequestat(owner, Keypress, Kret, 0, &pos);
else if(op == 15)
res = ownerrequestat(owner, Keypress, Kesc, 0, &pos);
else if(op == 16)
res = ownerrequestat(owner, Keypress, Kdown, 0, &pos);
else if(op == 17)
res = ownerrequestat(owner, Keypress, Kup, 0, &pos);
else if(op == 18)
res = ownerrequestat(owner, Keypress, Ktab,
(rnd & 0x1000) ? Mshift : 0, &pos);
else if(op == 19)
res = ownerrequestat(owner, Keypress,
modes[(rnd >> 12) % nelem(modes)], Mctrl, &pos);
else if(op == 20)
res = ownerrequestat(owner, Keypress, 'e', Mctrl, &pos);
else if(op == 21)
res = ownerrequest(owner, Keyreset, 0, 0);
else if(op == 22)
res = ownerrequest(owner, Keyrelease, 0, 0);
else if(op == 23)
res = ownerrequestat(owner, Keycaret, 0, 0, &pos);
else if(op == 24)
res = ownerrequest(owner, Keypress,
Kmodfirst + ((rnd >> 20) & 7), 0);
else if(op == 25)
res = ownerrequestat(owner, Keypress, 'p', Mctrl, &pos);
else if(op == 26)
res = ownerrequestat(owner, Keypress, 0xf008, 0, &pos);
else
res = ownerrequestat(owner, Keypress,
'1' + ((rnd >> 11) % 9), 0, &pos);
if(!stressstr(&res.commit) || !stressstr(&res.preedit) ||
!stressstr(&im.pre) || !stressstr(&im.raw) ||
!stressstr(&search.raw) || !stressstr(&search.text) ||
im.l == nil || im.nkouho < 0 || im.nkouho > Maxkouho ||
im.sel < -1 || im.sel >= Maxkouho ||
(im.nkouho > 0 && im.sel >= im.nkouho)){
CT_ERRORF(t, "invalid engine state after stress operation %d", i);
break;
}
for(j = 0; j < im.nkouho; j++)
if(!stressstr(&im.kouho[j])){
CT_ERRORF(t, "invalid candidate after stress operation %d", i);
i = 16000;
break;
}
if(activeowner != 0 && activeowner != owners[0] &&
activeowner != owners[1] && activeowner != owners[2]){
CT_ERRORF(t, "unknown active owner after stress operation %d", i);
break;
}
}
emojiend(&f);
vi->map = vimap;
}
void
engine_full_boundary_passthrough(struct ct *t)
{
Str com;
int i;
init();
im.l = getlang(LangJP);
for(i = 0; i < Maxrunes; i++)
im.pre.r[i] = L'';
im.pre.n = Maxrunes;
sclear(&com);
CT_CHECK(t, !keystroke(' ', 0, &com));
CT_EQ_INT(t, Maxrunes, com.n);
for(i = 0; i < Maxrunes; i++)
CT_EQ_UINT(t, L'', com.r[i]);
CT_EQ_INT(t, 0, im.pre.n);
CT_EQ_INT(t, 0, im.raw.n);
init();
im.l = getlang(LangJP);
for(i = 0; i < Maxrunes-1; i++)
im.pre.r[i] = L'';
im.pre.n = Maxrunes-1;
sclear(&com);
CT_CHECK(t, keystroke(' ', 0, &com));
CT_EQ_INT(t, Maxrunes, com.n);
CT_EQ_UINT(t, ' ', com.r[Maxrunes-1]);
}

View File

@@ -76,3 +76,37 @@ hmap_long_utf8_keys(struct ct *t)
cleanup: cleanup:
hmapfree(h); hmapfree(h);
} }
void
hmap_binary_keys_and_invalid_lengths(struct ct *t)
{
Hmap *h;
Hnode *n;
Str key, other;
h = hmapalloc(1);
CT_CHECK(t, h != nil);
key.n = 3;
key.r[0] = 'a';
key.r[1] = 0;
key.r[2] = 'b';
other = key;
other.r[2] = 'c';
hmapset(&h, &key, "one", 3);
hmapset(&h, &other, "two", 3);
n = hmapget(h, &key);
if(CT_CHECK(t, n != nil)){
CT_EQ_INT(t, 3, n->klen);
CT_EQ_MEM(t, "one", n->val, n->vlen);
}
n = hmapget(h, &other);
if(CT_CHECK(t, n != nil))
CT_EQ_MEM(t, "two", n->val, n->vlen);
hmapset(&h, &key, "bad", -1);
hmapset(&h, &key, nil, 1);
n = hmapget(h, &key);
if(CT_CHECK(t, n != nil))
CT_EQ_MEM(t, "one", n->val, n->vlen);
CT_EQ_PTR(t, nil, hmapalloc(0));
hmapfree(h);
}

View File

@@ -1,3 +1,8 @@
#define _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <sys/socket.h>
#include <unistd.h>
#include "test.h" #include "test.h"
void void
@@ -12,4 +17,201 @@ ipc_masks_modifiers(struct ct *t)
CT_EQ_INT(t, 1, want); CT_EQ_INT(t, 1, want);
CT_EQ_UINT(t, Mctrl|Malt, mod); CT_EQ_UINT(t, Mctrl|Malt, mod);
CT_EQ_UINT(t, 0x1f642, key); CT_EQ_UINT(t, 0x1f642, key);
CT_CHECK(t, !ipcreqreset(buf));
ipcpackreset(buf, 1);
ipcunpackreq(buf, &want, &mod, &key);
CT_EQ_INT(t, 1, want);
CT_EQ_UINT(t, 0, mod);
CT_EQ_UINT(t, 0, key);
CT_CHECK(t, ipcreqreset(buf));
}
void
ipc_runtime_path(struct ct *t)
{
char buf[128], fallback[128], *old, *saved;
old = getenv("XDG_RUNTIME_DIR");
saved = old == nil ? nil : strdup(old);
setenv("XDG_RUNTIME_DIR", "/tmp/strans-runtime-test", 1);
CT_EQ_INT(t, 0, ipcpath(buf, sizeof buf));
CT_EQ_STR(t, "/tmp/strans-runtime-test/strans.sock", buf);
setenv("XDG_RUNTIME_DIR", "/tmp/strans-runtime-test/", 1);
CT_EQ_INT(t, 0, ipcpath(buf, sizeof buf));
CT_EQ_STR(t, "/tmp/strans-runtime-test/strans.sock", buf);
setenv("XDG_RUNTIME_DIR", "relative", 1);
CT_EQ_INT(t, 0, ipcpath(buf, sizeof buf));
snprint(fallback, sizeof fallback, "/tmp/strans.%d", getuid());
CT_EQ_STR(t, fallback, buf);
CT_EQ_INT(t, -1, ipcpath(buf, 4));
CT_EQ_INT(t, -1, ipcpath(nil, sizeof buf));
if(saved != nil){
setenv("XDG_RUNTIME_DIR", saved, 1);
free(saved);
}else
unsetenv("XDG_RUNTIME_DIR");
}
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' };
uchar out[Ipcmaxresp+1], field[Ipcfieldmax];
int n;
n = ipcpackresp(out, sizeof out, 7, "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);
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);
CT_EQ_INT(t, Ipcresphdrsz + Ipclensz, n);
memset(field, 'x', sizeof field);
memset(out, 0xa5, sizeof out);
n = ipcpackresp(out, Ipcmaxresp, 1,
(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, 0xa5, out[Ipcmaxresp]);
CT_EQ_INT(t, -1, ipcpackresp(out, Ipcmaxresp-1, 1,
(char*)field, sizeof field, (char*)field, sizeof field, 1));
CT_EQ_INT(t, -1, ipcpackresp(out, sizeof out, 0,
nil, 1, nil, 0, 0));
CT_EQ_INT(t, -1, ipcpackresp(out, sizeof out, 0,
nil, 0, nil, 1, 1));
CT_EQ_INT(t, -1, ipcpackresp(out, sizeof out, 0,
(char*)field, Ipcfieldmax+1, nil, 0, 0));
}
void
ipc_response_empty_and_preedit(struct ct *t)
{
uchar first[Ipcmaxresp], second[Ipcmaxresp];
char commit[16], preedit[16];
Ipcresp resp;
int fd[2], nfirst, nsecond;
nfirst = ipcpackresp(first, sizeof first, 0, nil, 0, nil, 0, 0);
nsecond = ipcpackresp(second, sizeof second, 1,
"go", 2, "kana", 4, 1);
if(!CT_CHECK(t, nfirst > 0 && nsecond > 0))
return;
if(socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0){
CT_ERRORF(t, "socketpair failed");
return;
}
if(CT_EQ_INT(t, 0, ipcsend(fd[0], first, nfirst)) &&
CT_EQ_INT(t, 0, ipcsend(fd[0], second, nsecond)) &&
CT_EQ_INT(t, 0, ipcreadresp(fd[1], 0, commit, sizeof commit,
nil, 0, &resp))){
CT_EQ_INT(t, 0, resp.eaten);
CT_EQ_SIZE(t, 0, resp.ncommit);
CT_EQ_SIZE(t, 0, resp.npreedit);
CT_EQ_STR(t, "", commit);
}
if(CT_EQ_INT(t, 0, ipcreadresp(fd[1], 1, commit, sizeof commit,
preedit, sizeof preedit, &resp))){
CT_EQ_INT(t, 1, resp.eaten);
CT_EQ_SIZE(t, 2, resp.ncommit);
CT_EQ_SIZE(t, 4, resp.npreedit);
CT_EQ_STR(t, "go", commit);
CT_EQ_STR(t, "kana", preedit);
}
close(fd[0]);
close(fd[1]);
}
void
ipc_response_max_and_drain(struct ct *t)
{
uchar first[Ipcmaxresp], second[Ipcmaxresp], field[Ipcfieldmax];
char commit[8], preedit[8];
Ipcresp resp;
int fd[2], nfirst, nsecond;
memset(field, 'x', sizeof field);
nfirst = ipcpackresp(first, sizeof first, 1,
(char*)field, sizeof field, (char*)field, sizeof field, 1);
nsecond = ipcpackresp(second, sizeof second, 0, "ok", 2, nil, 0, 0);
if(!CT_CHECK(t, nfirst == Ipcmaxresp && nsecond > 0))
return;
if(socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0){
CT_ERRORF(t, "socketpair failed");
return;
}
if(CT_EQ_INT(t, 0, ipcsend(fd[0], first, nfirst)) &&
CT_EQ_INT(t, 0, ipcsend(fd[0], second, nsecond)) &&
CT_EQ_INT(t, 0, ipcreadresp(fd[1], 1, commit, sizeof commit,
preedit, sizeof preedit, &resp))){
CT_EQ_SIZE(t, Ipcfieldmax, resp.ncommit);
CT_EQ_SIZE(t, Ipcfieldmax, resp.npreedit);
CT_EQ_STR(t, "xxxxxxx", commit);
CT_EQ_STR(t, "xxxxxxx", preedit);
}
if(CT_EQ_INT(t, 0, ipcreadresp(fd[1], 0, commit, sizeof commit,
nil, 0, &resp)))
CT_EQ_STR(t, "ok", commit);
close(fd[0]);
close(fd[1]);
}
void
ipc_response_fragmented_and_truncated(struct ct *t)
{
uchar frame[Ipcmaxresp];
char commit[8], preedit[8];
Ipcresp resp;
int fd[2], i, n;
n = ipcpackresp(frame, sizeof frame, 1, "abc", 3, "xy", 2, 1);
if(!CT_CHECK(t, n > 0))
return;
if(socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0){
CT_ERRORF(t, "socketpair failed");
return;
}
for(i = 0; i < n; i++)
if(ipcsend(fd[0], frame+i, 1) < 0){
CT_ERRORF(t, "fragment send failed");
break;
}
if(i == n && CT_EQ_INT(t, 0, ipcreadresp(fd[1], 1,
commit, sizeof commit, preedit, sizeof preedit, &resp))){
CT_EQ_STR(t, "abc", commit);
CT_EQ_STR(t, "xy", preedit);
}
close(fd[0]);
close(fd[1]);
if(socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0){
CT_ERRORF(t, "socketpair failed");
return;
}
CT_EQ_INT(t, 0, ipcsend(fd[0], frame, n-1));
shutdown(fd[0], SHUT_WR);
CT_EQ_INT(t, -1, ipcreadresp(fd[1], 1, commit, sizeof commit,
preedit, sizeof preedit, &resp));
close(fd[0]);
close(fd[1]);
}
void
ipc_broken_peer_send(struct ct *t)
{
int fd[2];
if(socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0){
CT_ERRORF(t, "socketpair failed");
return;
}
close(fd[1]);
CT_EQ_INT(t, -1, ipcsend(fd[0], "x", 1));
close(fd[0]);
} }

View File

@@ -1,5 +1,27 @@
#include "test.h" #include "test.h"
static Str
kotrans(char *keys)
{
Emit e;
Im state;
Str out;
char *p;
memset(&state, 0, sizeof state);
state.l = getlang(LangKO);
sclear(&out);
for(p = keys; *p != '\0'; p++){
e = transko(&state, (uchar)*p);
sappend(&out, &e.s);
state.pre = e.next;
if(!e.eat)
sputr(&out, (uchar)*p);
}
sappend(&out, &state.pre);
return out;
}
void void
korean_sequences(struct ct *t) korean_sequences(struct ct *t)
{ {
@@ -14,39 +36,70 @@ korean_sequences(struct ct *t)
{ "Rk", "" }, { "Rk", "" },
{ "rk1", "가1" }, { "rk1", "가1" },
}; };
Emit e;
Im state;
Str out; Str out;
Rune key;
char *p;
int i; int i;
for(i = 0; i < nelem(cases); i++){ for(i = 0; i < nelem(cases); i++){
memset(&state, 0, sizeof state); out = kotrans(cases[i].keys);
state.l = getlang(LangKO);
sclear(&out);
for(p = cases[i].keys; *p != '\0'; p++){
key = (uchar)*p;
e = transko(&state, key);
sappend(&out, &e.s);
state.pre = e.next;
if(!e.eat)
sputr(&out, key);
}
sappend(&out, &state.pre);
checkstr(t, cases[i].keys, cases[i].want, &out); checkstr(t, cases[i].keys, cases[i].want, &out);
} }
} }
void
korean_compound_vowels(struct ct *t)
{
static const struct { char *keys, *want; } cases[] = {
{ "hk", "" }, { "ho", "" }, { "hl", "" },
{ "nj", "" }, { "np", "" }, { "nl", "" },
{ "ml", "" },
};
Str out;
int i;
for(i = 0; i < nelem(cases); i++){
out = kotrans(cases[i].keys);
checkstr(t, cases[i].keys, cases[i].want, &out);
}
}
void
korean_compound_finals(struct ct *t)
{
static const struct { char *keys, *want, *split; } cases[] = {
{ "rkrt", "", "각사" }, { "rksw", "", "간자" },
{ "rksg", "", "간하" }, { "rkfr", "", "갈가" },
{ "rkfa", "", "갈마" }, { "rkfq", "", "갈바" },
{ "rkft", "", "갈사" }, { "rkfx", "", "갈타" },
{ "rkfv", "", "갈파" }, { "rkfg", "", "갈하" },
{ "rkqt", "", "갑사" },
};
char keys[16];
Str out;
int i;
for(i = 0; i < nelem(cases); i++){
out = kotrans(cases[i].keys);
checkstr(t, cases[i].keys, cases[i].want, &out);
snprint(keys, sizeof keys, "%sk", cases[i].keys);
out = kotrans(keys);
checkstr(t, keys, cases[i].split, &out);
}
}
void void
korean_backspace(struct ct *t) korean_backspace(struct ct *t)
{ {
static const struct { char *before, *after; } cases[] = { static const struct { char *before, *after; } cases[] = {
{ "", "" }, { "", "" },
{ "", "" }, { "", "" },
{ "", "" }, { "", "" }, { "", "" }, { "", "" },
{ "", "" }, { "", "" }, { "", "" },
{ "", "" },
{ "", "" }, { "", "" },
{ "", "" }, { "", "" }, { "", "" }, { "", "" },
{ "", "" }, { "", "" }, { "", "" },
{ "", "" }, { "", "" }, { "", "" },
{ "", "" }, { "", "" },
}; };
Im state; Im state;
int i; int i;

View File

@@ -66,6 +66,7 @@ class MkemojiTest(unittest.TestCase):
"x\t a\n", "x\t a\n",
"\0\ta\n", "\0\ta\n",
"x\ta\0b\n", "x\ta\0b\n",
"x\t;hidden\n",
"x" * 65 + "\ta\n", "x" * 65 + "\ta\n",
"x\t" + "a" * 65 + "\n", "x\t" + "a" * 65 + "\n",
] ]

34
tests/popup_test.c Normal file
View File

@@ -0,0 +1,34 @@
#include "test.h"
#include <limits.h>
void
popup_layout(struct ct *t)
{
Rune heart[] = { 0x2764, 0xfe0f };
Caret caret;
int x, y;
CT_EQ_INT(t, 1, popupcells(heart, nelem(heart)));
memset(&caret, 0, sizeof caret);
popupposition(&caret, 40, 50, 800, 600, 100, 60, &x, &y);
CT_EQ_INT(t, 50, x);
CT_EQ_INT(t, 60, y);
caret.valid = 1;
caret.x = 70;
caret.y = 80;
caret.h = 20;
popupposition(&caret, 40, 50, 800, 600, 100, 60, &x, &y);
CT_EQ_INT(t, 70, x);
CT_EQ_INT(t, 100, y);
caret.x = 790;
caret.y = 590;
popupposition(&caret, 0, 0, 800, 600, 100, 60, &x, &y);
CT_EQ_INT(t, 700, x);
CT_EQ_INT(t, 540, y);
caret.x = INT_MIN;
caret.y = INT_MAX;
caret.h = INT_MAX;
popupposition(&caret, 0, 0, 800, 600, 100, 60, &x, &y);
CT_EQ_INT(t, 0, x);
CT_EQ_INT(t, 540, y);
}

58
tests/skk2ktrans_test.py Executable file
View File

@@ -0,0 +1,58 @@
#!/usr/bin/env python3
import subprocess
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
CONVERTER = ROOT / "map" / "skk2ktrans"
FIXTURES = Path(__file__).parent / "data" / "skk"
class Skk2KtransTest(unittest.TestCase):
def test_euc_jp_conversion_is_stable_and_literal(self):
source = (FIXTURES / "source.skk.utf8").read_text(encoding="utf-8")
expected = (FIXTURES / "expected.tsv").read_bytes()
with tempfile.TemporaryDirectory() as directory:
input_path = Path(directory) / "source.skk"
input_path.write_bytes(source.encode("euc_jp"))
result = subprocess.run(
[CONVERTER, input_path], capture_output=True, check=False)
self.assertEqual(result.returncode, 0, result.stderr.decode())
self.assertEqual(result.stdout, expected)
def test_invalid_euc_jp_is_rejected(self):
result = subprocess.run(
[CONVERTER], input=b"\xff\xff\n", capture_output=True, check=False)
self.assertNotEqual(result.returncode, 0)
self.assertTrue(result.stderr)
def test_escaped_candidate_is_rejected(self):
source = b"key /escaped\\/slash/\n"
result = subprocess.run(
[CONVERTER], input=source, capture_output=True, check=False)
self.assertNotEqual(result.returncode, 0)
self.assertIn(b"escaped candidates are unsupported", result.stderr)
def test_expression_candidate_is_rejected(self):
source = b'key /(concat "a/b")/literal/\n'
result = subprocess.run(
[CONVERTER], input=source, capture_output=True, check=False)
self.assertNotEqual(result.returncode, 0)
self.assertIn(b"expression candidates are unsupported", result.stderr)
def test_files_have_distinct_row_boundaries(self):
with tempfile.TemporaryDirectory() as directory:
first = Path(directory) / "first.skk"
second = Path(directory) / "second.skk"
first.write_bytes(b"a /A/")
second.write_bytes(b"b /B/\n")
result = subprocess.run(
[CONVERTER, first, second], capture_output=True, check=False)
self.assertEqual(result.returncode, 0, result.stderr.decode())
self.assertEqual(result.stdout, b"a\tA\nb\tB\n")
if __name__ == "__main__":
unittest.main()

View File

@@ -82,3 +82,21 @@ str_utf8_capacity(struct ct *t)
CT_EQ_STR(t, "😀", buf); CT_EQ_STR(t, "😀", buf);
CT_EQ_INT(t, 'Z', buf[5]); CT_EQ_INT(t, 'Z', buf[5]);
} }
void
str_invalid_and_full_appends(struct ct *t)
{
Str s;
int i;
sinit(&s, nil, 1);
CT_EQ_INT(t, 0, s.n);
sinit(&s, "x", -1);
CT_EQ_INT(t, 0, s.n);
for(i = 0; i < Maxrunes; i++)
s.r[i] = 'a';
s.n = Maxrunes;
sputr(&s, 'z');
CT_EQ_INT(t, Maxrunes, s.n);
CT_EQ_INT(t, 'a', s.r[Maxrunes-1]);
}

View File

@@ -1,12 +1,6 @@
#ifndef STRANS_TEST_H
#define STRANS_TEST_H
#include "../cutest/cutest.h" #include "../cutest/cutest.h"
#ifndef STRANS_TEST_ENGINE
#include "../dat.h" #include "../dat.h"
#include "../fn.h" #include "../fn.h"
#endif
extern Lang testvi; extern Lang testvi;
@@ -17,12 +11,21 @@ Str shownpre(Im*);
void str_init_utf8(struct ct*); void str_init_utf8(struct ct*);
void str_edit_and_alias(struct ct*); void str_edit_and_alias(struct ct*);
void str_utf8_capacity(struct ct*); void str_utf8_capacity(struct ct*);
void str_invalid_and_full_appends(struct ct*);
void hmap_set_replace_and_grow(struct ct*); void hmap_set_replace_and_grow(struct ct*);
void hmap_long_utf8_keys(struct ct*); void hmap_long_utf8_keys(struct ct*);
void hmap_binary_keys_and_invalid_lengths(struct ct*);
void trie_exact_prefix_and_duplicate(struct ct*); void trie_exact_prefix_and_duplicate(struct ct*);
void trie_optional_outputs_and_invalid_lengths(struct ct*);
void wayland_press_release_state(struct ct*);
void wayland_repeat_state(struct ct*);
void wayland_repeat_cancellation(struct ct*);
void popup_layout(struct ct*);
void production_maps_load(struct ct*); void production_maps_load(struct ct*);
void transmap_states(struct ct*); void transmap_states(struct ct*);
void korean_sequences(struct ct*); void korean_sequences(struct ct*);
void korean_compound_vowels(struct ct*);
void korean_compound_finals(struct ct*);
void korean_backspace(struct ct*); void korean_backspace(struct ct*);
void vietnamese_transitions(struct ct*); void vietnamese_transitions(struct ct*);
void vietnamese_backspace(struct ct*); void vietnamese_backspace(struct ct*);
@@ -30,18 +33,36 @@ void vietnamese_state_lifetime(struct ct*);
void engine_clears_candidates(struct ct*); void engine_clears_candidates(struct ct*);
void engine_backspace_clears_candidates(struct ct*); void engine_backspace_clears_candidates(struct ct*);
void engine_selects_visible_candidate(struct ct*); void engine_selects_visible_candidate(struct ct*);
void engine_candidate_shortcut_modifiers(struct ct*);
void engine_dictionary_queue_latest_wins(struct ct*);
void engine_active_owner_lifecycle(struct ct*);
void engine_active_owner_caret(struct ct*);
void engine_commit_contract(struct ct*); void engine_commit_contract(struct ct*);
void engine_language_switch_state(struct ct*); void engine_language_switch_state(struct ct*);
void engine_telex_history_bound(struct ct*); void engine_telex_history_bound(struct ct*);
void engine_korean_modifiers_and_backspace(struct ct*);
void engine_direct_language_modes(struct ct*);
void engine_japanese_readings(struct ct*);
void engine_japanese_candidates(struct ct*);
void engine_japanese_backspace_and_boundaries(struct ct*);
void engine_katakana_sequences(struct ct*);
void engine_emoji_single_candidate(struct ct*); void engine_emoji_single_candidate(struct ct*);
void engine_emoji_queries(struct ct*); void engine_emoji_queries(struct ct*);
void engine_emoji_japanese_and_multirune(struct ct*);
void engine_emoji_digit_aliases(struct ct*); void engine_emoji_digit_aliases(struct ct*);
void engine_emoji_navigation(struct ct*); void engine_emoji_navigation(struct ct*);
void engine_emoji_preedit_languages(struct ct*); void engine_emoji_preedit_languages(struct ct*);
void engine_emoji_start_and_unknown(struct ct*); void engine_emoji_start_and_unknown(struct ct*);
void engine_emoji_dictionary_identity(struct ct*); void engine_emoji_dictionary_identity(struct ct*);
void engine_randomized_stress(struct ct*);
void engine_full_boundary_passthrough(struct ct*);
void dictionary_candidates(struct ct*); void dictionary_candidates(struct ct*);
void dictionary_misses_clear_result(struct ct*); void dictionary_misses_clear_result(struct ct*);
void dictionary_emoji_identity(struct ct*);
void ipc_masks_modifiers(struct ct*); void ipc_masks_modifiers(struct ct*);
void ipc_runtime_path(struct ct*);
#endif void ipc_response_pack_boundaries(struct ct*);
void ipc_response_empty_and_preedit(struct ct*);
void ipc_response_max_and_drain(struct ct*);
void ipc_response_fragmented_and_truncated(struct ct*);
void ipc_broken_peer_send(struct ct*);

View File

@@ -24,8 +24,18 @@ checkstr(struct ct *t, char *where, char *want, Str *got)
Str Str
shownpre(Im *state) shownpre(Im *state)
{ {
Str shown; Str mapped, shown;
if(state->l->lang == LangJP || state->l->lang == LangJPK){
shown = state->pre;
if(state->raw.n == 0)
return shown;
if(mapget(state->l->map, &state->raw, &mapped))
sappend(&shown, &mapped);
else
sappend(&shown, &state->raw);
return shown;
}
if(state->l->map != nil && mapget(state->l->map, &state->pre, &shown)) if(state->l->map != nil && mapget(state->l->map, &state->pre, &shown))
return shown; return shown;
return state->pre; return state->pre;

View File

@@ -24,16 +24,34 @@ trie_exact_prefix_and_duplicate(struct ct *t)
goto cleanup; goto cleanup;
CT_EQ_INT(t, 6, n); CT_EQ_INT(t, 6, n);
CT_EQ_MEM(t, "second", v, n); CT_EQ_MEM(t, "second", v, n);
v = trieget(trie, "tabs", 4, &n);
if(!CT_CHECK(t, v != nil))
goto cleanup;
CT_EQ_INT(t, 7, n);
CT_EQ_MEM(t, "one\ttwo", v, n);
CT_EQ_PTR(t, nil, trieget(trie, "missing", 7, &n)); CT_EQ_PTR(t, nil, trieget(trie, "missing", 7, &n));
cleanup: cleanup:
trieclose(trie); trieclose(trie);
} }
void
trie_optional_outputs_and_invalid_lengths(struct ct *t)
{
char *v;
Trie *trie;
int n;
trie = trieopen("data/trie.map");
CT_CHECK(t, trieget(trie, "a", 1, nil) != nil);
v = nil;
CT_CHECK(t, trielookup(trie, "a", 1, &v, nil));
CT_CHECK(t, v != nil);
n = -1;
CT_CHECK(t, trielookup(trie, "a", 1, nil, &n));
CT_EQ_INT(t, 5, n);
CT_CHECK(t, trielookup(trie, "dupli", 5, nil, nil));
CT_EQ_PTR(t, nil, trieget(trie, nil, 1, &n));
CT_EQ_PTR(t, nil, trieget(trie, "a", -1, &n));
CT_CHECK(t, !trielookup(trie, nil, 1, &v, &n));
CT_CHECK(t, !trielookup(trie, "a", -1, &v, &n));
trieclose(trie);
}
void void
production_maps_load(struct ct *t) production_maps_load(struct ct *t)
{ {
@@ -76,10 +94,14 @@ transmap_states(struct ct *t)
}; };
Emit e; Emit e;
Im state; Im state;
Trie *fixture, *saved;
int i; int i;
fixture = trieopen("data/hira.map");
memset(&state, 0, sizeof state); memset(&state, 0, sizeof state);
state.l = getlang(LangJP); state.l = getlang(LangJP);
saved = state.l->map;
state.l->map = fixture;
for(i = 0; i < nelem(cases); i++){ for(i = 0; i < nelem(cases); i++){
state.pre = mkstr(cases[i].pre); state.pre = mkstr(cases[i].pre);
e = transmap(&state, cases[i].key); e = transmap(&state, cases[i].key);
@@ -90,4 +112,6 @@ transmap_states(struct ct *t)
checkstr(t, "next", cases[i].next, &e.next); checkstr(t, "next", cases[i].next, &e.next);
checkstr(t, "mapped", cases[i].mapped, &e.dict); checkstr(t, "mapped", cases[i].mapped, &e.dict);
} }
trieclose(fixture);
state.l->map = saved;
} }

View File

@@ -27,10 +27,9 @@ emalloc(ulong n)
{ {
void *p; void *p;
p = malloc(n); p = mallocz(n, 1);
if(p == nil) if(p == nil)
die("out of memory"); die("out of memory");
memset(p, 0, n);
return p; return p;
} }
@@ -46,12 +45,14 @@ erealloc(void *p, ulong n)
static void static void
testmapinit(void) testmapinit(void)
{ {
Lang *jp; Lang *jp, *kata;
jp = getlang(LangJP); jp = getlang(LangJP);
if(jp == nil) kata = getlang(LangJPK);
if(jp == nil || kata == nil)
die("test language is not registered"); die("test language is not registered");
jp->map = trieopen("data/hira.map"); jp->map = trieopen("../map/hira.map");
kata->map = trieopen("../map/kata.map");
memset(&testvi, 0, sizeof testvi); memset(&testvi, 0, sizeof testvi);
testvi.lang = LangVI; testvi.lang = LangVI;
testvi.mapname = "telex"; testvi.mapname = "telex";
@@ -64,12 +65,21 @@ static const struct ct_test tests[] = {
{ "str/init-utf8", str_init_utf8 }, { "str/init-utf8", str_init_utf8 },
{ "str/edit-and-alias", str_edit_and_alias }, { "str/edit-and-alias", str_edit_and_alias },
{ "str/utf8-capacity", str_utf8_capacity }, { "str/utf8-capacity", str_utf8_capacity },
{ "str/invalid-full-appends", str_invalid_and_full_appends },
{ "hmap/set-replace-grow", hmap_set_replace_and_grow }, { "hmap/set-replace-grow", hmap_set_replace_and_grow },
{ "hmap/long-utf8-keys", hmap_long_utf8_keys }, { "hmap/long-utf8-keys", hmap_long_utf8_keys },
{ "hmap/binary-invalid-lengths", hmap_binary_keys_and_invalid_lengths },
{ "trie/exact-prefix-duplicate", trie_exact_prefix_and_duplicate }, { "trie/exact-prefix-duplicate", trie_exact_prefix_and_duplicate },
{ "trie/optional-invalid-lengths", trie_optional_outputs_and_invalid_lengths },
{ "wayland/press-release", wayland_press_release_state },
{ "wayland/repeat", wayland_repeat_state },
{ "wayland/repeat-cancellation", wayland_repeat_cancellation },
{ "popup/layout", popup_layout },
{ "map/production-lifecycle", production_maps_load }, { "map/production-lifecycle", production_maps_load },
{ "transmap/states", transmap_states }, { "transmap/states", transmap_states },
{ "hangul/sequences", korean_sequences }, { "hangul/sequences", korean_sequences },
{ "hangul/compound-vowels", korean_compound_vowels },
{ "hangul/compound-finals", korean_compound_finals },
{ "hangul/backspace", korean_backspace }, { "hangul/backspace", korean_backspace },
{ "telex/transitions", vietnamese_transitions }, { "telex/transitions", vietnamese_transitions },
{ "telex/backspace", vietnamese_backspace }, { "telex/backspace", vietnamese_backspace },
@@ -77,19 +87,39 @@ static const struct ct_test tests[] = {
{ "engine/clears-candidates", engine_clears_candidates }, { "engine/clears-candidates", engine_clears_candidates },
{ "engine/backspace-clears-candidates", engine_backspace_clears_candidates }, { "engine/backspace-clears-candidates", engine_backspace_clears_candidates },
{ "engine/selects-visible-candidate", engine_selects_visible_candidate }, { "engine/selects-visible-candidate", engine_selects_visible_candidate },
{ "engine/candidate-shortcut-modifiers", engine_candidate_shortcut_modifiers },
{ "engine/dictionary-queue-latest", engine_dictionary_queue_latest_wins },
{ "engine/active-owner-lifecycle", engine_active_owner_lifecycle },
{ "engine/active-owner-caret", engine_active_owner_caret },
{ "engine/commit-contract", engine_commit_contract }, { "engine/commit-contract", engine_commit_contract },
{ "engine/language-switch-state", engine_language_switch_state }, { "engine/language-switch-state", engine_language_switch_state },
{ "engine/telex-history-bound", engine_telex_history_bound }, { "engine/telex-history-bound", engine_telex_history_bound },
{ "engine/korean-modifiers-backspace", engine_korean_modifiers_and_backspace },
{ "engine/direct-language-modes", engine_direct_language_modes },
{ "engine/japanese-readings", engine_japanese_readings },
{ "engine/japanese-candidates", engine_japanese_candidates },
{ "engine/japanese-backspace-boundaries", engine_japanese_backspace_and_boundaries },
{ "engine/katakana-sequences", engine_katakana_sequences },
{ "engine/emoji-single-candidate", engine_emoji_single_candidate }, { "engine/emoji-single-candidate", engine_emoji_single_candidate },
{ "engine/emoji-queries", engine_emoji_queries }, { "engine/emoji-queries", engine_emoji_queries },
{ "engine/emoji-japanese-multirune", engine_emoji_japanese_and_multirune },
{ "engine/emoji-digit-aliases", engine_emoji_digit_aliases }, { "engine/emoji-digit-aliases", engine_emoji_digit_aliases },
{ "engine/emoji-navigation", engine_emoji_navigation }, { "engine/emoji-navigation", engine_emoji_navigation },
{ "engine/emoji-preedit-languages", engine_emoji_preedit_languages }, { "engine/emoji-preedit-languages", engine_emoji_preedit_languages },
{ "engine/emoji-start-and-unknown", engine_emoji_start_and_unknown }, { "engine/emoji-start-and-unknown", engine_emoji_start_and_unknown },
{ "engine/emoji-dictionary-identity", engine_emoji_dictionary_identity }, { "engine/emoji-dictionary-identity", engine_emoji_dictionary_identity },
{ "engine/randomized-stress", engine_randomized_stress },
{ "engine/full-boundary-passthrough", engine_full_boundary_passthrough },
{ "dict/candidates", dictionary_candidates }, { "dict/candidates", dictionary_candidates },
{ "dict/misses-clear-result", dictionary_misses_clear_result }, { "dict/misses-clear-result", dictionary_misses_clear_result },
{ "dict/emoji-identity", dictionary_emoji_identity },
{ "ipc/masks-modifiers", ipc_masks_modifiers }, { "ipc/masks-modifiers", ipc_masks_modifiers },
{ "ipc/runtime-path", ipc_runtime_path },
{ "ipc/response-pack-boundaries", ipc_response_pack_boundaries },
{ "ipc/response-empty-preedit", ipc_response_empty_and_preedit },
{ "ipc/response-max-drain", ipc_response_max_and_drain },
{ "ipc/response-fragmented-truncated", ipc_response_fragmented_and_truncated },
{ "ipc/broken-peer-send", ipc_broken_peer_send },
}; };
void void

66
tests/wayland_test.c Normal file
View File

@@ -0,0 +1,66 @@
#include "test.h"
void
wayland_press_release_state(struct ct *t)
{
Wlstate s;
wlstateinit(&s);
CT_CHECK(t, wlstatepress(&s, 30, 1, 0, 0));
CT_CHECK(t, !wlstaterelease(&s, 30));
CT_CHECK(t, wlstaterelease(&s, 30));
CT_CHECK(t, wlstatepress(&s, 31, 0, 0, 0));
CT_CHECK(t, wlstaterelease(&s, 31));
CT_CHECK(t, !wlstatepress(&s, Wlkeymax, 1, 0, 0));
CT_CHECK(t, wlstaterelease(&s, Wlkeymax));
}
void
wayland_repeat_state(struct ct *t)
{
Wlstate s;
u32int key;
wlstateinit(&s);
wlstaterepeatinfo(&s, 25, 400);
CT_CHECK(t, wlstatepress(&s, 14, 1, 1, 100));
CT_EQ_INT(t, 400, wlstatetimeout(&s, 100));
CT_CHECK(t, !wlstaterepeat(&s, 499, &key));
CT_CHECK(t, wlstaterepeat(&s, 500, &key));
CT_EQ_UINT(t, 14, key);
CT_EQ_INT(t, 40, wlstatetimeout(&s, 500));
CT_CHECK(t, !wlstaterelease(&s, 14));
CT_EQ_INT(t, -1, wlstatetimeout(&s, 500));
CT_CHECK(t, wlstatepress(&s, 15, 1, 0, 600));
CT_EQ_INT(t, -1, wlstatetimeout(&s, 600));
CT_CHECK(t, !wlstaterelease(&s, 15));
CT_CHECK(t, wlstatepress(&s, 16, 0, 1, 600));
CT_EQ_INT(t, -1, wlstatetimeout(&s, 600));
CT_CHECK(t, wlstaterelease(&s, 16));
}
void
wayland_repeat_cancellation(struct ct *t)
{
Wlstate s;
wlstateinit(&s);
wlstaterepeatinfo(&s, 20, 300);
CT_CHECK(t, wlstatepress(&s, 20, 1, 1, 0));
CT_CHECK(t, wlstatepress(&s, 19, 0, 0, 10));
CT_EQ_INT(t, -1, wlstatetimeout(&s, 10));
CT_CHECK(t, !wlstaterelease(&s, 20));
CT_CHECK(t, wlstaterelease(&s, 19));
CT_CHECK(t, wlstatepress(&s, 20, 1, 1, 0));
wlstaterepeatinfo(&s, 30, 200);
CT_EQ_INT(t, -1, wlstatetimeout(&s, 0));
CT_CHECK(t, !wlstaterelease(&s, 20));
CT_CHECK(t, wlstatepress(&s, 21, 1, 1, 0));
wlstatecancelrepeat(&s);
CT_EQ_INT(t, -1, wlstatetimeout(&s, 0));
CT_CHECK(t, !wlstaterelease(&s, 21));
CT_CHECK(t, wlstatepress(&s, 22, 1, 1, 0));
wlstateclear(&s);
CT_CHECK(t, wlstaterelease(&s, 22));
}

82
tests/xim_test.c Normal file
View File

@@ -0,0 +1,82 @@
#include <assert.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <xcb-imdkit/encoding.h>
#include <xkbcommon/xkbcommon-keysyms.h>
uint32_t keymaplookup(const uint32_t*, int, uint16_t);
char *ximcompound(const char*, size_t, size_t*);
enum
{
ShiftMask = 1<<0,
LockMask = 1<<1,
Group1 = 1<<13,
};
static void
checktext(const char *s)
{
char *ct, *utf8;
size_t clen, len, ulen;
len = strlen(s);
ct = ximcompound(s, len, &clen);
assert(ct != NULL);
utf8 = xcb_compound_text_to_utf8(ct, clen, &ulen);
assert(utf8 != NULL);
assert(ulen == len);
assert(memcmp(utf8, s, len) == 0);
free(utf8);
free(ct);
}
static void
checkutf8run(void)
{
static const char s[] = "😀";
char *ct;
size_t n;
ct = ximcompound(s, sizeof s - 1, &n);
assert(ct != NULL);
assert(n >= 6);
assert(memcmp(ct, "\033%G", 3) == 0);
assert(memcmp(ct+n-3, "\033%@", 3) == 0);
free(ct);
}
int
main(void)
{
static const uint32_t letters[] = {
XKB_KEY_a, XKB_KEY_A, XKB_KEY_Cyrillic_ef, XKB_KEY_Cyrillic_EF,
};
static const uint32_t inferred[] = { XKB_KEY_a, XKB_KEY_NoSymbol };
static const uint32_t punctuation[] = { XKB_KEY_1, XKB_KEY_exclam };
static const uint32_t missing[] = {
XKB_KEY_a, XKB_KEY_A, XKB_KEY_NoSymbol, XKB_KEY_NoSymbol,
};
xcb_compound_text_init();
assert(keymaplookup(letters, 4, 0) == XKB_KEY_a);
assert(keymaplookup(letters, 4, ShiftMask) == XKB_KEY_A);
assert(keymaplookup(letters, 4, LockMask) == XKB_KEY_A);
assert(keymaplookup(letters, 4, LockMask|ShiftMask) == XKB_KEY_a);
assert(keymaplookup(letters, 4, Group1) == XKB_KEY_Cyrillic_ef);
assert(keymaplookup(letters, 4, Group1|ShiftMask) == XKB_KEY_Cyrillic_EF);
assert(keymaplookup(inferred, 2, ShiftMask) == XKB_KEY_A);
assert(keymaplookup(punctuation, 2, LockMask) == XKB_KEY_1);
assert(keymaplookup(punctuation, 2, LockMask|ShiftMask) == XKB_KEY_exclam);
assert(keymaplookup(missing, 4, Group1) == XKB_KEY_a);
assert(keymaplookup(NULL, 0, 0) == XKB_KEY_NoSymbol);
checktext("한글");
checktext("😀");
checktext("❤️");
checktext("A😀한");
checkutf8run();
return 0;
}

18
trie.c
View File

@@ -80,18 +80,23 @@ trieopen(char *path)
vlen = strlen(line); vlen = strlen(line);
if(vlen > 0 && line[vlen-1] == '\r') if(vlen > 0 && line[vlen-1] == '\r')
line[--vlen] = '\0'; line[--vlen] = '\0';
if(line[0] == '\0'){ if(line[0] == '\0' || line[0] == ';'){
free(line); free(line);
continue; continue;
} }
tab = strchr(line, '\t'); tab = strchr(line, '\t');
if(tab == nil || tab[1] == '\0') if(tab == nil || tab == line || tab[1] == '\0' ||
strchr(tab+1, '\t') != nil)
die("malformed map: %s", path); die("malformed map: %s", path);
*tab = '\0'; *tab = '\0';
key = line; key = line;
klen = tab - line; klen = tab - line;
if(utflen(key) > Maxrunes)
die("map key too long: %s", path);
val = tab + 1; val = tab + 1;
vlen = strlen(val); vlen = strlen(val);
if(utflen(val) > Maxrunes)
die("map value too long: %s", path);
insert(t, key, klen, val, vlen); insert(t, key, klen, val, vlen);
free(line); free(line);
} }
@@ -118,6 +123,8 @@ trieget(Trie *t, char *key, int klen, int *vlen)
int ni; int ni;
int i; int i;
if(t == nil || klen < 0 || (klen > 0 && key == nil))
return nil;
ni = t->root; ni = t->root;
for(i = 0; i < klen; i++){ for(i = 0; i < klen; i++){
ni = find(t, ni, key[i]); ni = find(t, ni, key[i]);
@@ -126,6 +133,7 @@ trieget(Trie *t, char *key, int klen, int *vlen)
} }
if(t->nodes[ni].val == nil) if(t->nodes[ni].val == nil)
return nil; return nil;
if(vlen != nil)
*vlen = t->nodes[ni].vlen; *vlen = t->nodes[ni].vlen;
return t->nodes[ni].val; return t->nodes[ni].val;
} }
@@ -136,14 +144,18 @@ trielookup(Trie *t, char *key, int klen, char **val, int *vlen)
int ni; int ni;
int i; int i;
if(t == nil || klen < 0 || (klen > 0 && key == nil))
return 0;
ni = t->root; ni = t->root;
for(i = 0; i < klen; i++){ for(i = 0; i < klen; i++){
ni = find(t, ni, key[i]); ni = find(t, ni, key[i]);
if(ni < 0) if(ni < 0)
return 0; return 0;
} }
if(val != nil && t->nodes[ni].val != nil){ if(t->nodes[ni].val != nil){
if(val != nil)
*val = t->nodes[ni].val; *val = t->nodes[ni].val;
if(vlen != nil)
*vlen = t->nodes[ni].vlen; *vlen = t->nodes[ni].vlen;
} }
return 1; return 1;

372
wayland.c
View File

@@ -1,18 +1,19 @@
#include "dat.h" #include "dat.h"
#include "fn.h" #include "fn.h"
#include <stdio.h> #include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <poll.h> #include <poll.h>
#include <string.h>
#include <sys/mman.h> #include <sys/mman.h>
#include <time.h>
#include <unistd.h>
#include <wayland-client.h> #include <wayland-client.h>
#include <xkbcommon/xkbcommon.h> #include <xkbcommon/xkbcommon.h>
#include "input-method-unstable-v2-client-protocol.h" #include "input-method-unstable-v2-client-protocol.h"
#include "virtual-keyboard-unstable-v1-client-protocol.h" #include "virtual-keyboard-unstable-v1-client-protocol.h"
static struct wl_display *dpy; static struct wl_display *dpy;
static struct wl_registry *reg;
static struct wl_seat *seat; static struct wl_seat *seat;
static struct zwp_input_method_manager_v2 *immgr; static struct zwp_input_method_manager_v2 *immgr;
static struct zwp_virtual_keyboard_manager_v1 *vkmgr; static struct zwp_virtual_keyboard_manager_v1 *vkmgr;
@@ -24,14 +25,34 @@ static struct xkb_keymap *keymap;
static struct xkb_state *xkbst; static struct xkb_state *xkbst;
static int active; static int active;
static int pending; static int pending;
static int newactivation;
static int running;
static u32int imserial; static u32int imserial;
static u32int seatname;
static u32int immgrname;
static u32int vkmgrname;
static Channel *replyc; static Channel *replyc;
static uvlong owner;
static Wlstate keys;
static uvlong
nowms(void)
{
struct timespec ts;
if(clock_gettime(CLOCK_MONOTONIC, &ts) < 0)
return 0;
return (uvlong)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}
static void static void
sendkey(u32int ks, u32int mod, Keyres *res) sendkey(u32int ks, u32int mod, Keyres *res)
{ {
Keyreq kr; Keyreq kr;
memset(&kr, 0, sizeof kr);
kr.owner = owner;
kr.op = Keypress;
kr.ks = ks; kr.ks = ks;
kr.mod = mod; kr.mod = mod;
kr.want = 1; kr.want = 1;
@@ -41,11 +62,20 @@ sendkey(u32int ks, u32int mod, Keyres *res)
} }
static void static void
sendreset(void) releaseowner(void)
{ {
Keyreq kr;
Keyres res; Keyres res;
sendkey(Kesc, 0, &res); if(owner == 0)
return;
memset(&kr, 0, sizeof kr);
kr.owner = owner;
kr.op = Keyrelease;
kr.reply = replyc;
chansend(keyc, &kr);
chanrecv(replyc, &res);
owner = 0;
} }
static u32int static u32int
@@ -85,47 +115,101 @@ mget(void)
} }
static void static void
kpress(uint32_t time, uint32_t keycode, uint32_t state) putresult(Keyres *res)
{
char commit[Maxutf], preedit[Maxutf];
int plen;
if(im == nil)
return;
stoutf(&res->commit, commit, sizeof commit);
stoutf(&res->preedit, preedit, sizeof preedit);
if(commit[0] != '\0')
zwp_input_method_v2_commit_string(im, commit);
plen = strlen(preedit);
zwp_input_method_v2_set_preedit_string(im, preedit, plen, plen);
zwp_input_method_v2_commit(im, imserial);
}
static void
forwardkey(u32int time, u32int keycode, u32int state)
{
if(vk != nil)
zwp_virtual_keyboard_v1_key(vk, time, keycode, state);
}
static void
keypress(u32int time, u32int keycode)
{ {
xkb_keysym_t ks; xkb_keysym_t ks;
u32int k, mod; u32int k, mod;
Keyres res; Keyres res;
char commit[Maxutf], preedit[Maxutf]; int repeatable;
int plen;
if(state != WL_KEYBOARD_KEY_STATE_PRESSED || xkbst == nil){ if(xkbst == nil || keycode >= Wlkeymax){
zwp_virtual_keyboard_v1_key(vk, time, keycode, state); forwardkey(time, keycode, WL_KEYBOARD_KEY_STATE_PRESSED);
wlstatepress(&keys, keycode, 0, 0, nowms());
return; return;
} }
ks = xkb_state_key_get_one_sym(xkbst, keycode + 8); ks = xkb_state_key_get_one_sym(xkbst, keycode + 8);
k = kget(ks); k = kget(ks);
if(k == 0){ if(k == 0){
zwp_virtual_keyboard_v1_key(vk, time, keycode, state); forwardkey(time, keycode, WL_KEYBOARD_KEY_STATE_PRESSED);
wlstatepress(&keys, keycode, 0, 0, nowms());
return; return;
} }
mod = mget(); mod = mget();
sendkey(k, mod, &res); sendkey(k, mod, &res);
stoutf(&res.commit, commit, sizeof commit); putresult(&res);
stoutf(&res.preedit, preedit, sizeof preedit); repeatable = keymap != nil &&
if(commit[0] != '\0'){ xkb_keymap_key_repeats(keymap, keycode + 8);
zwp_input_method_v2_commit_string(im, commit); wlstatepress(&keys, keycode, res.eaten, repeatable, nowms());
zwp_input_method_v2_commit(im, imserial);
}
plen = strlen(preedit);
zwp_input_method_v2_set_preedit_string(im, preedit, plen, plen);
zwp_input_method_v2_commit(im, imserial);
if(!res.eaten) if(!res.eaten)
zwp_virtual_keyboard_v1_key(vk, time, keycode, state); forwardkey(time, keycode, WL_KEYBOARD_KEY_STATE_PRESSED);
}
static void
keyrepeat(u32int time, u32int keycode)
{
xkb_keysym_t ks;
u32int k;
Keyres res;
if(xkbst == nil)
return;
ks = xkb_state_key_get_one_sym(xkbst, keycode + 8);
k = kget(ks);
if(k == 0)
return;
sendkey(k, mget(), &res);
putresult(&res);
/* The original eaten press still owns its eventual physical release. */
if(!res.eaten){
forwardkey(time, keycode, WL_KEYBOARD_KEY_STATE_PRESSED);
forwardkey(time, keycode, WL_KEYBOARD_KEY_STATE_RELEASED);
}
}
static void
kpress(u32int time, u32int keycode, u32int state)
{
if(state == WL_KEYBOARD_KEY_STATE_PRESSED){
keypress(time, keycode);
return;
}
if(wlstaterelease(&keys, keycode))
forwardkey(time, keycode, state);
} }
static void static void
kg_keymap(void *data, struct zwp_input_method_keyboard_grab_v2 *g, kg_keymap(void *data, struct zwp_input_method_keyboard_grab_v2 *g,
uint32_t format, int32_t fd, uint32_t size) u32int format, int32_t fd, u32int size)
{ {
char *s; char *s;
(void)data; (void)data;
(void)g; (void)g;
wlstatecancelrepeat(&keys);
if(format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1){ if(format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1){
close(fd); close(fd);
return; return;
@@ -145,13 +229,14 @@ kg_keymap(void *data, struct zwp_input_method_keyboard_grab_v2 *g,
xkbst = xkb_state_new(keymap); xkbst = xkb_state_new(keymap);
munmap(s, size); munmap(s, size);
} }
if(vk != nil)
zwp_virtual_keyboard_v1_keymap(vk, format, fd, size); zwp_virtual_keyboard_v1_keymap(vk, format, fd, size);
close(fd); close(fd);
} }
static void static void
kg_key(void *data, struct zwp_input_method_keyboard_grab_v2 *g, kg_key(void *data, struct zwp_input_method_keyboard_grab_v2 *g,
uint32_t serial, uint32_t time, uint32_t key, uint32_t state) u32int serial, u32int time, u32int key, u32int state)
{ {
(void)data; (void)data;
(void)g; (void)g;
@@ -161,14 +246,14 @@ kg_key(void *data, struct zwp_input_method_keyboard_grab_v2 *g,
static void static void
kg_mods(void *data, struct zwp_input_method_keyboard_grab_v2 *g, kg_mods(void *data, struct zwp_input_method_keyboard_grab_v2 *g,
uint32_t serial, uint32_t dep, uint32_t lat, uint32_t lck, u32int serial, u32int dep, u32int lat, u32int lck, u32int group)
uint32_t group)
{ {
(void)data; (void)data;
(void)g; (void)g;
(void)serial; (void)serial;
if(xkbst != nil) if(xkbst != nil)
xkb_state_update_mask(xkbst, dep, lat, lck, 0, 0, group); xkb_state_update_mask(xkbst, dep, lat, lck, 0, 0, group);
if(vk != nil)
zwp_virtual_keyboard_v1_modifiers(vk, dep, lat, lck, group); zwp_virtual_keyboard_v1_modifiers(vk, dep, lat, lck, group);
} }
@@ -178,8 +263,7 @@ kg_repeat(void *data, struct zwp_input_method_keyboard_grab_v2 *g,
{ {
(void)data; (void)data;
(void)g; (void)g;
(void)rate; wlstaterepeatinfo(&keys, rate, delay);
(void)delay;
} }
static const struct zwp_input_method_keyboard_grab_v2_listener static const struct zwp_input_method_keyboard_grab_v2_listener
@@ -196,6 +280,7 @@ im_activate(void *data, struct zwp_input_method_v2 *m)
(void)data; (void)data;
(void)m; (void)m;
pending = 1; pending = 1;
newactivation = 1;
} }
static void static void
@@ -204,11 +289,12 @@ im_deactivate(void *data, struct zwp_input_method_v2 *m)
(void)data; (void)data;
(void)m; (void)m;
pending = 0; pending = 0;
wlstateclear(&keys);
} }
static void static void
im_surrounding(void *data, struct zwp_input_method_v2 *m, im_surrounding(void *data, struct zwp_input_method_v2 *m,
const char *text, uint32_t cursor, uint32_t anchor) const char *text, u32int cursor, u32int anchor)
{ {
(void)data; (void)data;
(void)m; (void)m;
@@ -218,7 +304,7 @@ im_surrounding(void *data, struct zwp_input_method_v2 *m,
} }
static void static void
im_textchange(void *data, struct zwp_input_method_v2 *m, uint32_t cause) im_textchange(void *data, struct zwp_input_method_v2 *m, u32int cause)
{ {
(void)data; (void)data;
(void)m; (void)m;
@@ -227,7 +313,7 @@ im_textchange(void *data, struct zwp_input_method_v2 *m, uint32_t cause)
static void static void
im_content(void *data, struct zwp_input_method_v2 *m, im_content(void *data, struct zwp_input_method_v2 *m,
uint32_t hint, uint32_t purpose) u32int hint, u32int purpose)
{ {
(void)data; (void)data;
(void)m; (void)m;
@@ -236,25 +322,41 @@ im_content(void *data, struct zwp_input_method_v2 *m,
} }
static void static void
im_done(void *data, struct zwp_input_method_v2 *m) endactivation(void)
{ {
(void)data; wlstateclear(&keys);
(void)m;
imserial++;
if(pending && !active){
active = 1;
grab = zwp_input_method_v2_grab_keyboard(im);
if(grab != nil)
zwp_input_method_keyboard_grab_v2_add_listener(grab,
&grab_listener, nil);
}else if(!pending && active){
active = 0; active = 0;
sendreset(); releaseowner();
if(grab != nil){ if(grab != nil){
zwp_input_method_keyboard_grab_v2_release(grab); zwp_input_method_keyboard_grab_v2_release(grab);
grab = nil; grab = nil;
} }
} }
static void
beginactivation(void)
{
if(active)
endactivation();
active = 1;
owner = ownernew();
grab = zwp_input_method_v2_grab_keyboard(im);
if(grab != nil)
zwp_input_method_keyboard_grab_v2_add_listener(grab,
&grab_listener, nil);
}
static void
im_done(void *data, struct zwp_input_method_v2 *m)
{
(void)data;
(void)m;
imserial++;
if(newactivation && pending)
beginactivation();
else if(!pending && active)
endactivation();
newactivation = 0;
} }
static void static void
@@ -262,6 +364,8 @@ im_unavail(void *data, struct zwp_input_method_v2 *m)
{ {
(void)data; (void)data;
(void)m; (void)m;
wlstateclear(&keys);
running = 0;
fprint(2, "strans: wayland: input-method unavailable\n"); fprint(2, "strans: wayland: input-method unavailable\n");
} }
@@ -276,28 +380,34 @@ static const struct zwp_input_method_v2_listener im_listener = {
}; };
static void static void
reg_global(void *data, struct wl_registry *r, uint32_t name, reg_global(void *data, struct wl_registry *r, u32int name,
const char *iface, uint32_t version) const char *iface, u32int version)
{ {
(void)data; (void)data;
if(strcmp(iface, wl_seat_interface.name) == 0) if(strcmp(iface, wl_seat_interface.name) == 0 && seat == nil){
seatname = name;
seat = wl_registry_bind(r, name, &wl_seat_interface, seat = wl_registry_bind(r, name, &wl_seat_interface,
version < 5 ? version : 5); version < 5 ? version : 5);
else if(strcmp(iface, zwp_input_method_manager_v2_interface.name) == 0) }else if(strcmp(iface,
zwp_input_method_manager_v2_interface.name) == 0 && immgr == nil){
immgrname = name;
immgr = wl_registry_bind(r, name, immgr = wl_registry_bind(r, name,
&zwp_input_method_manager_v2_interface, 1); &zwp_input_method_manager_v2_interface, 1);
else if(strcmp(iface, }else if(strcmp(iface,
zwp_virtual_keyboard_manager_v1_interface.name) == 0) zwp_virtual_keyboard_manager_v1_interface.name) == 0 && vkmgr == nil){
vkmgrname = name;
vkmgr = wl_registry_bind(r, name, vkmgr = wl_registry_bind(r, name,
&zwp_virtual_keyboard_manager_v1_interface, 1); &zwp_virtual_keyboard_manager_v1_interface, 1);
} }
}
static void static void
reg_remove(void *data, struct wl_registry *r, uint32_t name) reg_remove(void *data, struct wl_registry *r, u32int name)
{ {
(void)data; (void)data;
(void)r; (void)r;
(void)name; if(name == seatname || name == immgrname || name == vkmgrname)
running = 0;
} }
static const struct wl_registry_listener reg_listener = { static const struct wl_registry_listener reg_listener = {
@@ -305,37 +415,159 @@ static const struct wl_registry_listener reg_listener = {
.global_remove = reg_remove, .global_remove = reg_remove,
}; };
void static int
waylandthread(void *_) dispatch(void)
{ {
struct wl_registry *reg; struct pollfd pfd;
u32int key;
uvlong now;
int flush, timeout, n;
(void)_; pfd.fd = wl_display_get_fd(dpy);
threadsetname("wayland"); while(running){
while((dpy = wl_display_connect(nil)) == nil) if(wl_display_dispatch_pending(dpy) < 0)
poll(nil, 0, 1000); return -1;
xkb = xkb_context_new(XKB_CONTEXT_NO_FLAGS); if(!running)
if(xkb == nil){ break;
now = nowms();
if(wlstaterepeat(&keys, now, &key))
keyrepeat((u32int)now, key);
while(wl_display_prepare_read(dpy) != 0)
if(wl_display_dispatch_pending(dpy) < 0)
return -1;
flush = wl_display_flush(dpy);
if(flush < 0 && errno != EAGAIN){
wl_display_cancel_read(dpy);
return -1;
}
pfd.events = POLLIN;
if(flush < 0)
pfd.events |= POLLOUT;
pfd.revents = 0;
timeout = wlstatetimeout(&keys, nowms());
n = poll(&pfd, 1, timeout);
if(n < 0){
wl_display_cancel_read(dpy);
if(errno == EINTR)
continue;
return -1;
}
if(n == 0){
wl_display_cancel_read(dpy);
continue;
}
if(pfd.revents & POLLIN){
if(wl_display_read_events(dpy) < 0)
return -1;
}else
wl_display_cancel_read(dpy);
if(pfd.revents & (POLLERR | POLLHUP | POLLNVAL))
return -1;
if(pfd.revents & POLLOUT)
wl_display_flush(dpy);
}
return 0;
}
static void
resetconnection(void)
{
dpy = nil;
reg = nil;
seat = nil;
immgr = nil;
vkmgr = nil;
im = nil;
grab = nil;
vk = nil;
xkb = nil;
keymap = nil;
xkbst = nil;
active = 0;
pending = 0;
newactivation = 0;
running = 1;
imserial = 0;
seatname = 0;
immgrname = 0;
vkmgrname = 0;
owner = 0;
wlstateinit(&keys);
}
static void
cleanup(void)
{
running = 0;
wlstateclear(&keys);
releaseowner();
active = 0;
if(xkbst != nil)
xkb_state_unref(xkbst);
if(keymap != nil)
xkb_keymap_unref(keymap);
if(xkb != nil)
xkb_context_unref(xkb);
if(grab != nil)
wl_proxy_destroy((struct wl_proxy*)grab);
if(vk != nil)
wl_proxy_destroy((struct wl_proxy*)vk);
if(im != nil)
wl_proxy_destroy((struct wl_proxy*)im);
if(vkmgr != nil)
wl_proxy_destroy((struct wl_proxy*)vkmgr);
if(immgr != nil)
wl_proxy_destroy((struct wl_proxy*)immgr);
if(seat != nil)
wl_proxy_destroy((struct wl_proxy*)seat);
if(reg != nil)
wl_registry_destroy(reg);
if(dpy != nil){
wl_display_disconnect(dpy); wl_display_disconnect(dpy);
dpy = nil; dpy = nil;
return;
} }
}
static void
connectonce(void)
{
resetconnection();
dpy = wl_display_connect(nil);
if(dpy == nil)
return;
xkb = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
if(xkb == nil)
return;
reg = wl_display_get_registry(dpy); reg = wl_display_get_registry(dpy);
if(reg == nil)
return;
wl_registry_add_listener(reg, &reg_listener, nil); wl_registry_add_listener(reg, &reg_listener, nil);
wl_display_roundtrip(dpy); if(wl_display_roundtrip(dpy) < 0)
return;
if(!running)
return;
if(seat == nil || immgr == nil || vkmgr == nil){ if(seat == nil || immgr == nil || vkmgr == nil){
fprint(2, fprint(2,
"strans: wayland: compositor missing input-method-v2\n"); "strans: wayland: compositor missing input-method-v2\n");
wl_display_disconnect(dpy);
dpy = nil;
return; return;
} }
im = zwp_input_method_manager_v2_get_input_method(immgr, seat); im = zwp_input_method_manager_v2_get_input_method(immgr, seat);
zwp_input_method_v2_add_listener(im, &im_listener, nil);
vk = zwp_virtual_keyboard_manager_v1_create_virtual_keyboard(vkmgr, seat); vk = zwp_virtual_keyboard_manager_v1_create_virtual_keyboard(vkmgr, seat);
replyc = chancreate(sizeof(Keyres), 0); if(im == nil || vk == nil)
while(wl_display_dispatch(dpy) != -1) return;
; zwp_input_method_v2_add_listener(im, &im_listener, nil);
chanfree(replyc); dispatch();
replyc = nil; }
void
waylandthread(void *_)
{
(void)_;
threadsetname("wayland");
replyc = chancreate(sizeof(Keyres), 0);
for(;;){
connectonce();
cleanup();
poll(nil, 0, 1000);
}
} }

107
wayland_state.c Normal file
View File

@@ -0,0 +1,107 @@
#include "dat.h"
#include "fn.h"
#include <limits.h>
#include <string.h>
enum {
Downforward = 1,
Downeaten = 2,
};
void
wlstateinit(Wlstate *s)
{
memset(s, 0, sizeof *s);
}
void
wlstatecancelrepeat(Wlstate *s)
{
s->repeating = 0;
s->repeatkey = 0;
s->repeatat = 0;
}
void
wlstateclear(Wlstate *s)
{
memset(s->down, 0, sizeof s->down);
wlstatecancelrepeat(s);
}
void
wlstaterepeatinfo(Wlstate *s, int rate, int delay)
{
/* Settings changes end the old schedule; a later press starts a new one. */
wlstatecancelrepeat(s);
s->rate = rate > 0 ? rate : 0;
s->delay = delay > 0 ? delay : 0;
}
int
wlstatepress(Wlstate *s, u32int key, int eaten, int repeatable, uvlong now)
{
uvlong delay;
if(key >= Wlkeymax)
return 0;
s->down[key] = eaten ? Downeaten : Downforward;
wlstatecancelrepeat(s);
if(eaten && repeatable && s->rate > 0){
delay = s->delay;
if(~(uvlong)0 - now < delay)
s->repeatat = ~(uvlong)0;
else
s->repeatat = now + delay;
s->repeatkey = key;
s->repeating = 1;
}
return 1;
}
int
wlstaterelease(Wlstate *s, u32int key)
{
int forward;
if(key >= Wlkeymax)
return 1;
forward = s->down[key] != Downeaten;
s->down[key] = 0;
if(s->repeating && s->repeatkey == key)
wlstatecancelrepeat(s);
return forward;
}
int
wlstatetimeout(Wlstate *s, uvlong now)
{
uvlong d;
if(!s->repeating)
return -1;
if(now >= s->repeatat)
return 0;
d = s->repeatat - now;
return d > INT_MAX ? INT_MAX : (int)d;
}
int
wlstaterepeat(Wlstate *s, uvlong now, u32int *key)
{
uvlong step;
if(!s->repeating || now < s->repeatat)
return 0;
if(key != 0)
*key = s->repeatkey;
step = 1000 / s->rate;
if(step == 0)
step = 1;
if(~(uvlong)0 - now < step)
s->repeatat = ~(uvlong)0;
else
s->repeatat = now + step;
return 1;
}

149
win.c
View File

@@ -12,6 +12,7 @@ static xcb_connection_t *conn;
static xcb_screen_t *scr; static xcb_screen_t *scr;
static xcb_window_t win; static xcb_window_t win;
static xcb_gcontext_t gc; static xcb_gcontext_t gc;
static xcb_pixmap_t pix;
static u32int *img; static u32int *img;
static int depth; static int depth;
@@ -26,7 +27,70 @@ getscr(xcb_connection_t *c, int n)
return nil; return nil;
} }
static xcb_visualtype_t*
getvisual(xcb_screen_t *s)
{
xcb_depth_iterator_t di;
xcb_visualtype_iterator_t vi;
for(di = xcb_screen_allowed_depths_iterator(s); di.rem;
xcb_depth_next(&di)){
if(di.data->depth != s->root_depth)
continue;
for(vi = xcb_depth_visuals_iterator(di.data); vi.rem;
xcb_visualtype_next(&vi))
if(vi.data->visual_id == s->root_visual)
return vi.data;
}
return nil;
}
static int
validformat(xcb_connection_t *c, xcb_screen_t *s)
{
const xcb_setup_t *setup;
xcb_format_iterator_t fi;
xcb_visualtype_t *v;
u32int one;
int lsb;
setup = xcb_get_setup(c);
v = getvisual(s);
if(setup == nil || v == nil || s->root_depth != 24 ||
v->_class != XCB_VISUAL_CLASS_TRUE_COLOR ||
v->bits_per_rgb_value != 8 ||
v->red_mask != 0xff0000 || v->green_mask != 0x00ff00 ||
v->blue_mask != 0x0000ff)
return 0;
one = 1;
lsb = *(uchar*)&one != 0;
if((lsb && setup->image_byte_order != XCB_IMAGE_ORDER_LSB_FIRST) ||
(!lsb && setup->image_byte_order != XCB_IMAGE_ORDER_MSB_FIRST))
return 0;
for(fi = xcb_setup_pixmap_formats_iterator(setup); fi.rem;
xcb_format_next(&fi))
if(fi.data->depth == 24 && fi.data->bits_per_pixel == 32 &&
fi.data->scanline_pad == 32)
return 1;
return 0;
}
static void static void
wincleanup(void)
{
free(img);
img = nil;
if(conn != nil){
xcb_disconnect(conn);
conn = nil;
}
scr = nil;
win = 0;
gc = 0;
pix = 0;
}
static int
wininit(void) wininit(void)
{ {
int n; int n;
@@ -35,11 +99,17 @@ wininit(void)
xcb_intern_atom_reply_t *r1, *r2; xcb_intern_atom_reply_t *r1, *r2;
conn = xcb_connect(nil, &n); conn = xcb_connect(nil, &n);
if(conn == nil || xcb_connection_has_error(conn)) if(conn == nil || xcb_connection_has_error(conn)){
die("xcb_connect"); fprint(2, "strans: popup disabled: cannot connect to X display\n");
wincleanup();
return 0;
}
scr = getscr(conn, n); scr = getscr(conn, n);
if(scr == nil) if(scr == nil || !validformat(conn, scr)){
die("no screen"); fprint(2, "strans: popup disabled: unsupported X root format\n");
wincleanup();
return 0;
}
depth = scr->root_depth; depth = scr->root_depth;
win = xcb_generate_id(conn); win = xcb_generate_id(conn);
mask = XCB_CW_BACK_PIXEL | XCB_CW_BORDER_PIXEL | mask = XCB_CW_BACK_PIXEL | XCB_CW_BORDER_PIXEL |
@@ -65,17 +135,29 @@ wininit(void)
free(r2); free(r2);
gc = xcb_generate_id(conn); gc = xcb_generate_id(conn);
xcb_create_gc(conn, gc, win, 0, nil); xcb_create_gc(conn, gc, win, 0, nil);
pix = xcb_generate_id(conn);
xcb_create_pixmap(conn, depth, pix, win, Imgw, Imgh);
mask = XCB_CW_BACK_PIXMAP;
xcb_change_window_attributes(conn, win, mask, &pix);
img = emalloc(Imgw * Imgh * sizeof(img[0])); img = emalloc(Imgw * Imgh * sizeof(img[0]));
fontinit(fontdir); if(!fontinit(fontdir)){
fprint(2, "strans: popup disabled: no usable fonts\n");
wincleanup();
return 0;
}
return 1;
} }
static void static void
drawstr(u32int *buf, int x, int y, Rune *r, int n, int maxw, int maxh) drawstr(u32int *buf, int x, int y, Rune *r, int n, int maxw, int maxh)
{ {
while(n-- > 0){ while(n-- > 0){
putfont(buf, maxw, maxh, x, y, *r++); if(*r != 0xfe0e && *r != 0xfe0f){
putfont(buf, maxw, maxh, x, y, *r);
x += Fontsz; x += Fontsz;
} }
r++;
}
} }
static void static void
@@ -111,19 +193,20 @@ drawkouho(Drawcmd *dc, int first, int n, int w, int h)
static void static void
putimage(int w, int h) putimage(int w, int h)
{ {
xcb_put_image(conn, XCB_IMAGE_FORMAT_Z_PIXMAP, win, gc, xcb_put_image(conn, XCB_IMAGE_FORMAT_Z_PIXMAP, pix, gc,
w, h, 0, 0, 0, depth, w * h * 4, (u8int*)img); w, h, 0, 0, 0, depth, w * h * 4, (u8int*)img);
xcb_flush(conn); /* The retained background pixmap lets the server repaint exposures. */
xcb_clear_area(conn, 0, win, 0, 0, w, h);
} }
static void static int
winhide(void) winhide(void)
{ {
xcb_unmap_window(conn, win); xcb_unmap_window(conn, win);
xcb_flush(conn); return xcb_flush(conn) > 0;
} }
static void static int
winshow(Drawcmd *dc) winshow(Drawcmd *dc)
{ {
int npre, px, py, w, h, i, n, maxw; int npre, px, py, w, h, i, n, maxw;
@@ -134,23 +217,27 @@ winshow(Drawcmd *dc)
n = min(max(dc->nkouho, 0), Maxdisp); n = min(max(dc->nkouho, 0), Maxdisp);
npre = dc->pre.n != 0; npre = dc->pre.n != 0;
if(n == 0 && npre == 0){ if(n == 0 && npre == 0){
winhide(); return winhide();
return;
} }
cookie = xcb_query_pointer(conn, scr->root); maxw = popupcells(dc->pre.r, dc->pre.n);
maxw = dc->pre.n;
for(i = 0; i < n; i++) for(i = 0; i < n; i++)
maxw = max(maxw, dc->kouho[i].n); maxw = max(maxw, popupcells(dc->kouho[i].r, dc->kouho[i].n));
ptr = xcb_query_pointer_reply(conn, cookie, nil);
if(ptr == nil)
die("xcb_query_pointer");
px = ptr->root_x + 10;
py = ptr->root_y + 10;
free(ptr);
vals[3] = h = (n + npre) * Fontsz; vals[3] = h = (n + npre) * Fontsz;
vals[2] = w = (maxw + 3) * Fontsz; vals[2] = w = (maxw + 3) * Fontsz;
vals[1] = py = max(0, min(py, scr->height_in_pixels - h)); px = py = 0;
vals[0] = px = max(0, min(px, scr->width_in_pixels - w)); if(!dc->caret.valid){
cookie = xcb_query_pointer(conn, scr->root);
ptr = xcb_query_pointer_reply(conn, cookie, nil);
if(ptr == nil)
return 0;
px = ptr->root_x;
py = ptr->root_y;
free(ptr);
}
popupposition(&dc->caret, px, py, scr->width_in_pixels,
scr->height_in_pixels, w, h, &px, &py);
vals[1] = py;
vals[0] = px;
xcb_configure_window(conn, win, xcb_configure_window(conn, win,
XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y | XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y |
XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT, XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT,
@@ -158,6 +245,7 @@ winshow(Drawcmd *dc)
xcb_map_window(conn, win); xcb_map_window(conn, win);
drawkouho(dc, 0, n, w, h); drawkouho(dc, 0, n, w, h);
putimage(w, h); putimage(w, h);
return xcb_flush(conn) > 0;
} }
void void
@@ -166,13 +254,16 @@ drawthread(void*)
Drawcmd dc; Drawcmd dc;
threadsetname("draw"); threadsetname("draw");
wininit(); if(!wininit())
return;
while(chanrecv(drawc, &dc) > 0){ while(chanrecv(drawc, &dc) > 0){
while(channbrecv(drawc, &dc) > 0) while(channbrecv(drawc, &dc) > 0)
; ;
if(dc.nkouho == 0 && dc.pre.n == 0) if(dc.nkouho == 0 && dc.pre.n == 0){
winhide(); if(!winhide())
else break;
winshow(&dc); }else if(!winshow(&dc))
break;
} }
wincleanup();
} }

View File

@@ -6,6 +6,7 @@ PROG = strans-xim
SRCS = $(wildcard *.c) SRCS = $(wildcard *.c)
OBJS = $(SRCS:.c=.o) OBJS = $(SRCS:.c=.o)
TEST = xim_test
all: $(PROG) all: $(PROG)
@@ -14,7 +15,13 @@ $(PROG): $(OBJS) ../ipc.c ../ipc.h
$(OBJS): ../ipc.h $(OBJS): ../ipc.h
clean: $(TEST): ../tests/xim_test.c keymap.c ximtext.c
rm -f $(OBJS) $(PROG) $(CC) $(CFLAGS) -o $@ ../tests/xim_test.c keymap.c ximtext.c $(XIM_LIBS)
.PHONY: all clean check: $(TEST)
./$(TEST)
clean:
rm -f $(OBJS) $(PROG) $(TEST)
.PHONY: all check clean

37
xim/keymap.c Normal file
View File

@@ -0,0 +1,37 @@
#include <stdint.h>
#include <xkbcommon/xkbcommon.h>
enum
{
ShiftMask = 1<<0,
LockMask = 1<<1,
GroupShift = 13,
GroupMask = 3,
};
uint32_t
keymaplookup(const uint32_t *syms, int nsyms, uint16_t state)
{
uint32_t lo, hi, lower, upper;
int col, group, shift;
if(syms == NULL || nsyms <= 0)
return XKB_KEY_NoSymbol;
group = (state >> GroupShift) & GroupMask;
col = group * 2;
if(col >= nsyms || syms[col] == XKB_KEY_NoSymbol)
col = 0;
lo = syms[col];
if(lo == XKB_KEY_NoSymbol)
return XKB_KEY_NoSymbol;
hi = col + 1 < nsyms ? syms[col + 1] : XKB_KEY_NoSymbol;
lower = xkb_keysym_to_lower(lo);
upper = xkb_keysym_to_upper(lo);
if(hi == XKB_KEY_NoSymbol)
hi = lower != upper ? upper : lo;
shift = (state & ShiftMask) != 0;
/* Lock reverses Shift only for keys with an alphabetic case pair. */
if((state & LockMask) && lower != upper)
shift = !shift;
return shift ? hi : lo;
}

View File

@@ -2,22 +2,28 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <unistd.h> #include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <xcb/xcb.h> #include <xcb/xcb.h>
#include <xkbcommon/xkbcommon.h> #include <xkbcommon/xkbcommon.h>
#include <xcb-imdkit/imdkit.h> #include <xcb-imdkit/imdkit.h>
#include <xcb-imdkit/encoding.h> #include <xcb-imdkit/encoding.h>
#include "ipc.h" #include "ipc.h"
uint32_t keymaplookup(const uint32_t*, int, uint16_t);
char *ximcompound(const char*, size_t, size_t*);
typedef struct Ic Ic;
struct Ic
{
int fd;
};
static xcb_connection_t *conn; static xcb_connection_t *conn;
static xcb_im_t *xim; static xcb_im_t *xim;
static int srvfd = -1;
static xcb_keysym_t *kmap; static xcb_keysym_t *kmap;
static uint8_t minkc, maxkc; static uint8_t minkc, maxkc;
static uint8_t symsper; static uint8_t symsper;
static char *encs[] = {"COMPOUND_TEXT", "en_US.UTF-8", ""}; static char *encs[] = {"COMPOUND_TEXT"};
static uint32_t styles[] = { static uint32_t styles[] = {
XCB_IM_PreeditNothing | XCB_IM_StatusNothing, XCB_IM_PreeditNothing | XCB_IM_StatusNothing,
XCB_IM_PreeditNone | XCB_IM_StatusNone, XCB_IM_PreeditNone | XCB_IM_StatusNone,
@@ -37,7 +43,7 @@ kinit(void)
xcb_get_keyboard_mapping_reply_t *r; xcb_get_keyboard_mapping_reply_t *r;
const xcb_setup_t *setup; const xcb_setup_t *setup;
xcb_keysym_t *syms; xcb_keysym_t *syms;
int i, n; int n;
setup = xcb_get_setup(conn); setup = xcb_get_setup(conn);
if(setup == NULL) if(setup == NULL)
@@ -50,26 +56,22 @@ kinit(void)
die("keyboard mapping failed"); die("keyboard mapping failed");
symsper = r->keysyms_per_keycode; symsper = r->keysyms_per_keycode;
n = xcb_get_keyboard_mapping_keysyms_length(r); n = xcb_get_keyboard_mapping_keysyms_length(r);
kmap = malloc(n * sizeof(xcb_keysym_t)); syms = malloc(n * sizeof(xcb_keysym_t));
if(kmap == NULL) if(syms == NULL)
die("malloc failed"); die("malloc failed");
syms = xcb_get_keyboard_mapping_keysyms(r); memcpy(syms, xcb_get_keyboard_mapping_keysyms(r),
for(i = 0; i < n; i++) n * sizeof(xcb_keysym_t));
kmap[i] = syms[i]; free(kmap);
kmap = syms;
free(r); free(r);
} }
static uint32_t static uint32_t
kget(uint8_t kc, uint16_t state) kget(uint8_t kc, uint16_t state)
{ {
int col;
if(kmap == NULL || kc < minkc || kc > maxkc) if(kmap == NULL || kc < minkc || kc > maxkc)
return 0; return 0;
col = (state & Mshift) ? 1 : 0; return keymaplookup(kmap + (kc - minkc) * symsper, symsper, state);
if(col >= symsper)
col = 0;
return kmap[(kc - minkc) * symsper + col];
} }
static xcb_screen_t* static xcb_screen_t*
@@ -97,7 +99,7 @@ commit(xcb_im_input_context_t *ic, char *s, int len)
if(len == 0) if(len == 0)
return; return;
ct = xcb_utf8_to_compound_text(s, len, &clen); ct = ximcompound(s, len, &clen);
if(ct == NULL) if(ct == NULL)
return; return;
xcb_im_commit_string(xim, ic, XCB_XIM_LOOKUP_CHARS, ct, clen, 0); xcb_im_commit_string(xim, ic, XCB_XIM_LOOKUP_CHARS, ct, clen, 0);
@@ -106,51 +108,40 @@ commit(xcb_im_input_context_t *ic, char *s, int len)
} }
static void static void
srvclose(void) srvclose(Ic *state)
{ {
if(srvfd < 0) if(state == NULL || state->fd < 0)
return; return;
close(srvfd); close(state->fd);
srvfd = -1; state->fd = -1;
} }
static int static int
srvconnect(void) srvconnect(Ic *state)
{ {
struct sockaddr_un addr; if(state->fd >= 0)
int fd;
if(srvfd >= 0)
return 0; return 0;
fd = socket(AF_UNIX, SOCK_STREAM, 0); state->fd = ipcconnect();
if(fd < 0) if(state->fd < 0)
return -1; return -1;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
snprintf(addr.sun_path, sizeof(addr.sun_path), IPCPATH, getuid());
if(connect(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0){
close(fd);
return -1;
}
srvfd = fd;
return 0; return 0;
} }
static int static int
readresp(xcb_im_input_context_t *ic) readresp(Ic *state, xcb_im_input_context_t *ic)
{ {
char buf[Ipcfieldmax+1]; char buf[Ipcfieldmax+1];
Ipcresp resp; Ipcresp resp;
if(ipcreadresp(srvfd, 0, buf, sizeof buf, NULL, 0, &resp) < 0) if(ipcreadresp(state->fd, 0, buf, sizeof buf, NULL, 0, &resp) < 0)
return -1; return -1;
if(buf[0] != '\0') if(resp.ncommit > 0)
commit(ic, buf, strlen(buf)); commit(ic, buf, resp.ncommit);
return resp.eaten; return resp.eaten;
} }
static void static void
kpress(xcb_im_input_context_t *ic, xcb_key_press_event_t *ev) kpress(Ic *state, xcb_im_input_context_t *ic, xcb_key_press_event_t *ev)
{ {
unsigned char buf[Ipcreqsz]; unsigned char buf[Ipcreqsz];
uint32_t key, rune; uint32_t key, rune;
@@ -166,10 +157,10 @@ kpress(xcb_im_input_context_t *ic, xcb_key_press_event_t *ev)
key = rune; key = rune;
ipcpackreq(buf, 0, ev->state, key); ipcpackreq(buf, 0, ev->state, key);
eaten = 0; eaten = 0;
if(srvconnect() == 0){ if(srvconnect(state) == 0){
if(ipcsend(srvfd, buf, sizeof buf) < 0 || if(ipcsend(state->fd, buf, sizeof buf) < 0 ||
(eaten = readresp(ic)) < 0){ (eaten = readresp(state, ic)) < 0){
srvclose(); srvclose(state);
eaten = 0; eaten = 0;
} }
} }
@@ -179,15 +170,43 @@ kpress(xcb_im_input_context_t *ic, xcb_key_press_event_t *ev)
} }
static void static void
reset(xcb_im_input_context_t *ic) reset(Ic *state, xcb_im_input_context_t *ic, int release)
{ {
unsigned char buf[Ipcreqsz]; unsigned char buf[Ipcreqsz];
ipcpackreq(buf, 0, 0, Kesc); if(state == NULL || state->fd < 0)
if(srvconnect() < 0)
return; return;
if(ipcsend(srvfd, buf, sizeof buf) < 0 || readresp(ic) < 0) ipcpackreset(buf, 0);
srvclose(); if(ipcsend(state->fd, buf, sizeof buf) < 0 ||
readresp(state, ic) < 0)
release = 1;
if(release)
srvclose(state);
}
static void
icfree(void *p)
{
Ic *state;
state = p;
/* imdkit frees this data on context destruction and client loss. */
srvclose(state);
free(state);
}
static void
iccreate(xcb_im_input_context_t *ic)
{
Ic *state;
if(ic == NULL || xcb_im_input_context_get_data(ic) != NULL)
return;
state = calloc(1, sizeof *state);
if(state == NULL)
return;
state->fd = -1;
xcb_im_input_context_set_data(ic, state, icfree);
} }
static void static void
@@ -195,20 +214,34 @@ callback(xcb_im_t *im, xcb_im_client_t *client, xcb_im_input_context_t *ic,
const xcb_im_packet_header_fr_t *hdr, void *frame, void *arg, void *user) const xcb_im_packet_header_fr_t *hdr, void *frame, void *arg, void *user)
{ {
xcb_key_press_event_t *ev; xcb_key_press_event_t *ev;
Ic *state;
(void)im; (void)im;
(void)client; (void)client;
(void)frame; (void)frame;
(void)user; (void)user;
if(hdr->major_opcode == XCB_XIM_CREATE_IC){
iccreate(ic);
return;
}
if(ic == NULL)
return;
state = xcb_im_input_context_get_data(ic);
if(state == NULL)
return;
switch(hdr->major_opcode){ switch(hdr->major_opcode){
case XCB_XIM_FORWARD_EVENT: case XCB_XIM_FORWARD_EVENT:
ev = arg; ev = arg;
if(ev != NULL && (ev->response_type & ~0x80) == XCB_KEY_PRESS) if(ev != NULL && (ev->response_type & ~0x80) == XCB_KEY_PRESS)
kpress(ic, ev); kpress(state, ic, ev);
break;
case XCB_XIM_RESET_IC:
/* ResetIC ends this context's ownership, like focus loss. */
reset(state, ic, 1);
break; break;
case XCB_XIM_UNSET_IC_FOCUS: case XCB_XIM_UNSET_IC_FOCUS:
reset(ic); reset(state, ic, 1);
break; break;
} }
} }
@@ -224,7 +257,7 @@ ximinit(void)
st.nStyles = 2; st.nStyles = 2;
st.styles = styles; st.styles = styles;
enc.nEncodings = 3; enc.nEncodings = 1;
enc.encodings = encs; enc.encodings = encs;
xcb_compound_text_init(); xcb_compound_text_init();
conn = xcb_connect(NULL, &scr); conn = xcb_connect(NULL, &scr);
@@ -247,15 +280,19 @@ int
main(void) main(void)
{ {
xcb_generic_event_t *ev; xcb_generic_event_t *ev;
uint8_t type;
ximinit(); ximinit();
for(;;){ for(;;){
ev = xcb_wait_for_event(conn); ev = xcb_wait_for_event(conn);
if(ev == NULL) if(ev == NULL)
break; break;
type = ev->response_type & ~0x80;
if(type == XCB_MAPPING_NOTIFY)
kinit();
else
xcb_im_filter_event(xim, ev); xcb_im_filter_event(xim, ev);
free(ev); free(ev);
} }
srvclose();
return 0; return 0;
} }

41
xim/ximtext.c Normal file
View File

@@ -0,0 +1,41 @@
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <xcb-imdkit/encoding.h>
static char*
ximutf8compound(const char *s, size_t len, size_t *outlen)
{
static const char begin[] = "\033%G";
static const char end[] = "\033%@";
char *ct;
size_t n;
if(s == NULL || len > SIZE_MAX - (sizeof begin + sizeof end - 1))
return NULL;
n = len + sizeof begin + sizeof end - 2;
ct = malloc(n + 1);
if(ct == NULL)
return NULL;
memcpy(ct, begin, sizeof begin - 1);
memcpy(ct + sizeof begin - 1, s, len);
memcpy(ct + sizeof begin - 1 + len, end, sizeof end - 1);
ct[n] = '\0';
if(outlen != NULL)
*outlen = n;
return ct;
}
char*
ximcompound(const char *s, size_t len, size_t *outlen)
{
char *ct;
if(s == NULL)
return NULL;
ct = xcb_utf8_to_compound_text(s, len, outlen);
if(ct != NULL)
return ct;
/* X.Org Compound Text extension for characters in no legacy charset. */
return ximutf8compound(s, len, outlen);
}