18 KiB
Viet+
Vietnamese Input Method for Linux
Zero underline • No pre-edit buffer • Backspace-Replay sync • Built in Rust
What is Viet+?
Viet+ is a Vietnamese input method for Linux that takes a fundamentally different approach from every other IME: Direct Input.
Most Vietnamese IMEs use a pre-edit buffer — you type into a temporary buffer with an ugly underline, and the text only becomes real Vietnamese when you commit it. This causes:
- Duplicate text (buffer + committed)
- Underline distraction
- Broken copy/paste
- Desync between engine state and what's on screen
Viet+ eliminates all of this. Keystrokes are instantly converted to Unicode — what you type is what you see. No buffer. No underline. No duplication.
How It Works
Data Flow: Keypress to Screen
Physical Keyboard
│
▼
┌──────────────────────────────────────────────────────────────┐
│ Stage 1: KEY CAPTURE │
│ │
│ X11: XGrabKeyboard intercepts all key events │
│ evdev: /dev/input/event* reads kernel events │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ X11Capture │ │ evdev grab │ │ FocusIn/FocusOut │ │
│ │ (libX11.so) │ │ (libevdev) │ │ detection │ │
│ └─────────────┘ └──────────────┘ └──────────────────┘ │
└──────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ Stage 2: KEY ROUTING │
│ │
│ Modifier keys (Ctrl/Alt/Super) → forward directly │
│ Ctrl+Space → toggle Vietnamese ON/OFF │
│ Backspace → replay_backspace() │
│ Characters → replay_and_inject(ch) │
└──────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ Stage 3: BACKSPACE-REPLAY │
│ │
│ keystroke_history = ['c', 'h', 'a', 'o', 's'] │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────┐ │
│ │ Create FRESH engine │ │
│ │ Replay ALL keystrokes through it │ │
│ │ engine.buffer() = "cháo" ← correct output │ │
│ └──────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ screen_output = "cháo" │
│ diff = backspaces(0) + type("cháo") │
│ (or no change if screen already shows "cháo") │
└──────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ Stage 4: OUTPUT COMMANDS │
│ │
│ EngineEvent::Replace { backspaces: 4, insert: "cháo" } │
│ │ │
│ ▼ │
│ OutputCommand::Backspace(4) │
│ OutputCommand::Type("cháo") │
└──────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ Stage 5: KEY INJECTION │
│ │
│ X11 path: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. Ungrab keyboard (XUngrabKeyboard) │ │
│ │ 2. Send backspaces via XTestFakeKeyEvent │ │
│ │ 3. Set clipboard via XChangeProperty │ │
│ │ 4. Handle SelectionRequest events │ │
│ │ 5. Send Ctrl+V via XTestFakeKeyEvent │ │
│ │ 6. Regrab keyboard (XGrabKeyboard) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ uinput path (Wayland): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. Send backspaces via /dev/uinput (EV_KEY 14) │ │
│ │ 2. For ASCII: send keycodes via uinput │ │
│ │ 3. For Unicode: wl-copy + Ctrl+V via uinput │ │
│ └─────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
│
▼
Application receives keystrokes
and renders Vietnamese text on screen
The Backspace-Replay Pattern
This is Viet+'s core innovation. Traditional IMEs track state incrementally — each keystroke updates an internal buffer. But this buffer can desync from what's actually on screen (due to focus changes, external pastes, etc.).
Viet+ solves this by never tracking incremental state:
Traditional IME:
keystroke → update buffer → emit event → hope it matches screen
Viet+ (Backspace-Replay):
keystroke → add to history → replay ALL history in fresh engine → compute diff
On every keystroke:
- The keystroke is appended to
keystroke_history - A brand new
Engineis created - The entire history is replayed through it
- The engine's buffer is the correct screen output
- Viet+ computes the diff: how many backspaces to erase old text, what new text to type
This means:
- Zero state desync — always recomputed from scratch
- Self-healing — if anything goes wrong, the next keystroke fixes it
- Simple — no complex state tracking or synchronization
Architecture
vietc/
├── engine/ # Core Vietnamese composition engine
│ ├── engine.rs # Orchestrator + replay_keystrokes()
│ ├── telex.rs # Telex state machine (688 lines)
│ ├── vni.rs # VNI state machine (593 lines)
│ ├── english.rs # English auto-restore dictionary
│ └── spelling.rs # Vietnamese syllable validation
│
├── protocol/ # Keyboard capture & injection
│ ├── inject.rs # KeyInjector trait
│ ├── x11_capture.rs # XGrabKeyboard + XNextEvent loop
│ ├── x11_inject.rs # XTest injection + direct clipboard
│ └── wayland_im.rs # Wayland IM protocol
│
├── daemon/ # Main daemon process
│ ├── main.rs # Event loops, Backspace-Replay, CPU pinning
│ ├── config.rs # TOML config loader + hot reload
│ ├── app_state.rs # Per-app Vietnamese/English memory
│ └── display.rs # X11/Wayland/compositor detection
│
├── ui/ # System tray icon
│ └── main.rs # Tray + daemon launcher
│
├── cli/ # Interactive test harness
├── packaging/ # AppImage + deb build scripts
└── vietc.toml # Default configuration
Component Interaction
┌─────────────────────────────────────────────────────────────┐
│ vietc-tray │
│ (System tray icon, daemon launcher, password prompt) │
└───────────────────────┬─────────────────────────────────────┘
│ starts
▼
┌─────────────────────────────────────────────────────────────┐
│ vietc (daemon) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ Config │ │ App State │ │ Display │ │
│ │ (hot reload) │ │ (per-app) │ │ (X11/Wayland) │ │
│ └──────┬───────┘ └──────┬───────┘ └────────┬─────────┘ │
│ │ │ │ │
│ └─────────────────┼────────────────────┘ │
│ │ │
│ ┌──────▼──────┐ │
│ │ Event Loop │ │
│ │ │ │
│ │ X11: grab │ │
│ │ keyboard │ │
│ │ │ │
│ │ Process │ │
│ │ keystroke │ │
│ │ │ │
│ │ Replay all │ │
│ │ history │ │
│ │ │ │
│ │ Inject │ │
│ │ diff │ │
│ └─────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ vietc-engine │ │
│ │ TelexEngine / VniEngine / EnglishDict / Spelling │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ vietc-protocol │ │
│ │ X11Capture / X11Injector / UinputInjector / Wayland │ │
│ └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Input Methods
Telex
| Key | Result | Example |
|---|---|---|
aa |
â | tan → tân |
aw |
ă | tan → tăn |
ee |
ê | men → mên |
oo |
ô | to → tô |
ow |
ơ | to → tơ |
uw |
ư | tu → tư |
s |
á (sắc) | as → á |
f |
à (huyền) | af → à |
r |
ả (hỏi) | ar → ả |
x |
ã (ngã) | ax → ã |
j |
ạ (nặng) | aj → ạ |
dd |
đ | dd → đ |
VNI
| Key | Result |
|---|---|
a1 |
á |
a2 |
à |
a3 |
ả |
a4 |
ã |
a5 |
ạ |
a6 |
â |
a8 |
ă |
e6 |
ê |
o6 |
ô |
o7 |
ơ |
u7 |
ư |
Features
| Feature | How It Works |
|---|---|
| Direct Input | No pre-edit buffer. Keystrokes instantly become Unicode via XTest/uinput injection |
| Backspace-Replay | Replays entire keystroke history in a fresh engine on every keypress — zero state desync |
| Flexible Placement | Type tone/modifier at end of syllable (tranaf → trần) — engine scans backward to find the vowel |
| Smart Clusters | uo → ươ, ươ + o → uô, shape modifier overriding (â↔ă, ô↔ơ) |
| Auto-Restore | ~250 English words recognized — typing hello won't become Vietnamese. Triggered on space/ESC |
| ESC Undo | Strip all tones from current word instantly |
| Macro Expansion | ko → không, dc → được, custom shortcuts |
| Casing Preservation | SATS → SÁT, Saa → Sả — matches your typing pattern |
| App Memory | Per-app Vietnamese/English state, saved to overrides.toml |
| Hot Reload | Config changes apply without restart (polls mtime every 1.5s) |
| Focus Reset | FocusIn/FocusOut clears engine state — no stale injection on window switch |
| CPU Priority | Pins daemon to P-cores (0-3) + nice(-10) for low-latency input |
Installation
AppImage (recommended)
./Viet+-0.1.0-x86_64.AppImage
Includes daemon + tray + CLI + xclip. No special permissions needed on X11.
Debian/Ubuntu
sudo dpkg -i vietc_0.1.0-1_amd64.deb
Recommends: libxtst6, xclip
Manual
git clone https://git.khoavo.myds.me/vndangkhoa/vietc.git
cd vietc
make build-all
sudo make install
Configuration
Config file: ~/.config/vietc/config.toml or ./vietc.toml
input_method = "vni" # "vni" or "telex"
toggle_key = "space" # Ctrl+Space to toggle
start_enabled = false # English by default
grab = true # grab keyboard (AppImage)
[auto_restore]
enabled = true
trigger_keys = ["space", "escape"]
[app_state]
enabled = true
english_apps = ["code", "vim", "kitty", "foot"]
vietnamese_apps = ["telegram", "discord", "firefox"]
[macros]
ko = "không"
dc = "được"
vs = "với"
lm = "làm"
Building
make build-all # Build with X11 + Wayland
make test # Run 255+ tests
make deb # Build .deb package
make appimage # Build AppImage
License
MIT License — see LICENSE for details.
Made with love for the Vietnamese Linux community