- Add system tray (vietc-tray) to Flatpak build; command changed to
vietc-tray which spawns the daemon
- Desktop menu entry: Viet+ appears in app launcher for search/install/uninstall
- Tray fixes: find_sibling_binary tries {name}-daemon fallback for Flatpak;
is_daemon_running checks both vietc and vietc-daemon process names
- Native X11 _NET_ACTIVE_WINDOW via dlopen(libX11.so.6) — third fallback in
get_active_window_id() that works inside Flatpak sandbox (no xdotool/xprop)
- Update README with install/uninstall commands
- Update CHANGELOG
17 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 │
│ │
│ evdev: /dev/input/event* grabs keyboard (primary, reliable) │
│ X11: XRecord passive monitoring (fallback) │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ evdev grab │ │ X11Capture │ │ FocusIn/FocusOut │ │
│ │ (libevdev) │ │ (XRecord) │ │ 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) │
│ VNI/Telex control keys → consume when no match │
└──────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ Stage 3: BAMBOO ENGINE │
│ │
│ Transformation model: keystrokes produce composition │
│ changes. Marks and tones modify existing characters. │
│ Flexible backtracking scans up to 5 chars for vowels. │
│ Smart uo→ươ cluster with backtrack. │
│ Only emits Replace events when output actually changes. │
└──────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ Stage 4: KEY INJECTION │
│ │
│ ASCII: direct Linux keycodes via /dev/uinput │
│ Backspace: Linux keycode 14 via uinput │
│ Vietnamese Unicode: clipboard paste + trailing ASCII via │
│ uinput (split only at whitespace/punctuation boundary) │
│ uinput Ctrl+V via /dev/uinput (no X11 dependency) │
│ │
│ Fallback: vietc-uinputd Unix socket daemon (privileged) │
└──────────────────────────────────────────────────────────────┘
│
▼
Application receives keystrokes
and renders Vietnamese text on screen
Event Sourcing + Backspace-Replay
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+ uses Event Sourcing: every input action is recorded as a typed InputEvent (KeyTyped, Backspace, Flush, Paste) in an EventStore. On every keystroke, the entire event history is replayed from scratch through a fresh engine to compute the correct diff — no incremental state to desync.
Traditional IME:
keystroke → update buffer → emit event → hope it matches screen
Viet+ (Event Sourcing):
keystroke → append InputEvent → replay ALL events in fresh engine → compute diff
On every keystroke:
- The keystroke is appended as an
InputEventto theEventStore - A brand new
Engineis created - The entire event history is replayed through it via
Engine::replay_events() - The engine's buffer is the correct screen output
- Viet+ computes the diff:
Engine::replay_events_to_commands()returns Type/Backspace commands
This means:
- Zero state desync — always recomputed from scratch
- Self-healing — if anything goes wrong, the next keystroke fixes it
- Privacy-safe —
EventStore::pattern_hash()provides a sha256 of the event type sequence for pattern detection without any ability to recover original text - Simple — no complex state tracking or synchronization
Architecture
vietc/
├── engine/ # Vietnamese composition engine (bamboo-core Rust port)
│ ├── engine.rs # Orchestrator + replay_events(), replay_events_to_commands()
│ ├── event.rs # Event Sourcing: InputEvent, EventStore, Command
│ ├── bamboo.rs # Bamboo engine: transformation model, composition, tone placement
│ ├── input_method.rs # Telex/VNI rule definitions
│ └── spelling.rs # Vietnamese syllable validation
│
├── protocol/ # Keyboard capture & injection
│ ├── inject.rs # KeyInjector trait
│ ├── x11_capture.rs # XRecord keyboard capture via C helper
│ ├── x11_inject.rs # XTest injection + direct clipboard
│ ├── uinput_monitor.rs # /dev/uinput injection for ASCII + Unicode
│ ├── uinput_client.rs # Unix socket client for vietc-uinputd
│ └── 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
│
├── uinputd/ # Privileged uinput backspace daemon (VMK-style)
│ └── main.rs # Unix socket server for /dev/uinput injection
│
├── 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 | Example |
|---|---|---|
1 |
á (sắc) | a1 → á |
2 |
à (huyền) | a2 → à |
3 |
ả (hỏi) | a3 → ả |
4 |
ã (ngã) | a4 → ã |
5 |
ạ (nặng) | a5 → ạ |
6 |
â/ê/ô | a6 → â, e6 → ê, o6 → ô |
7 |
ơ/ư | o7 → ơ, u7 → ư |
8 |
ă | a8 → ă |
9 |
đ | d9 → đ |
Flexible typing: type the full syllable, then add marks/tone keys at the end. Example: nguye6n4 → nguyễn. The engine scans backward up to 5 characters to find the target vowel.
Features
| Feature | How It Works |
|---|---|
| Direct Input | No pre-edit buffer. Keystrokes instantly become text via uinput/XTest injection |
| Bamboo Engine | Transformation model ported from bamboo-core — composition, marks, tones, flexible backtracking |
| Flexible Backtrack | Type tone/modifier at end of syllable (tranaf → trần). Scans up to 5 chars backward |
| Smart Clusters | uo → ươ with backtrack (chuong7 → chương) |
| Tone Placement | Correct tone positioning for all Vietnamese diphthongs (io→gió, uâ→xuất, yê→nguyễn) |
| Macro Expansion | ko → không, dc → được, custom shortcuts |
| Casing Preservation | Tieengs → Tiếng, TIEENGS → TIẾNG |
| App Memory | Per-app Vietnamese/English state, saved to overrides.toml |
| Hot Reload | Config changes apply without restart (polls mtime every 1.5s) |
| Window-Switch Reset | Alt+Tab clears engine state — no stale composition across apps, even when focus events are missed. Uses xdotool or xprop fallback to detect window changes |
| CPU Priority | Pins daemon to P-cores (0-3) + nice(-10) for low-latency input |
| Uinput Daemon | Privileged vietc-uinputd for clean backspace injection (Unix socket, VMK-style) |
Installation
Flatpak (recommended)
System tray icon + daemon. Find "Viet+" in your app menu to launch, or run from terminal.
# Install
flatpak install --user --bundle VietPlus-x86_64.flatpak
# Launch via app menu, or:
flatpak run io.github.vietc.VietPlus
# Uninstall
flatpak uninstall --user io.github.vietc.VietPlus
Includes daemon + CLI + system tray + uinput daemon. Sandboxed — no system libraries are touched.
Build from Source (Flatpak)
git clone https://github.com/vndangkhoa/vietc.git
cd vietc/packaging/flatpak
bash build-flatpak.sh [version]
Requires Flatpak runtimes: org.gnome.Platform//50, org.gnome.Sdk//50, org.freedesktop.Sdk.Extension.rust-stable//25.08
flatpak install --user flathub org.gnome.Platform//50
flatpak install --user flathub org.gnome.Sdk//50
flatpak install --user flathub org.freedesktop.Sdk.Extension.rust-stable//25.08
The Flatpak bundle includes the system tray and desktop menu entry. Find "Viet+" in your app launcher to start it, or search for it to uninstall. Warning-free build — no #![allow()] needed.
See packaging/flatpak/FLATPAK_BUILD.md for detailed build instructions.
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 (evdev)
[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"
License
MIT License — see LICENSE for details.
Made with love for the Vietnamese Linux community