A modern Vietnamese Input Method Engine (IME) for Linux with direct Unicode input—no pre-edit buffer, no underlines.
Find a file
Khoa Vo 666f1b400e fix: non-blocking XPending event loop + auto re-grab on grab loss
- Use XPending() to check for events before XNextEvent (non-blocking)
- Add is_grabbed() and has_pending_events() public methods
- Auto re-grab keyboard when grab is silently lost (tray, WM focus)
- Fixes AppImage daemon receiving zero keystroke events
2026-06-26 09:17:47 +07:00
cli Optimize typing performance and preserve casing on replaced syllables 2026-06-25 19:59:46 +07:00
daemon fix: non-blocking XPending event loop + auto re-grab on grab loss 2026-06-26 09:17:47 +07:00
engine feat: implement Backspace-Replay pattern for perfect engine sync 2026-06-26 08:40:38 +07:00
packaging fix: non-blocking XPending event loop + auto re-grab on grab loss 2026-06-26 09:17:47 +07:00
protocol fix: non-blocking XPending event loop + auto re-grab on grab loss 2026-06-26 09:17:47 +07:00
scripts Fix typing race conditions with unified channel injection, add persistent logging, and align config schemas 2026-06-24 20:30:14 +07:00
ui Optimize typing performance and preserve casing on replaced syllables 2026-06-25 19:59:46 +07:00
.gitignore Gitignore runtime status file 2026-06-24 17:33:39 +07:00
Cargo.toml Viet+ v0.1.0 - Vietnamese Input Method for Linux 2026-06-24 10:13:10 +07:00
CHANGELOG.md docs: rewrite README with Backspace-Replay, add CHANGELOG.md 2026-06-26 08:42:37 +07:00
LICENSE Viet+ v0.1.0 - Vietnamese Input Method for Linux 2026-06-24 10:13:10 +07:00
Makefile Optimize typing performance and preserve casing on replaced syllables 2026-06-25 19:59:46 +07:00
README.md docs: rewrite README with full architecture, data flow, and Backspace-Replay explanation 2026-06-26 09:01:05 +07:00
vietc.service Viet+ v0.1.1 2026-06-24 17:29:12 +07:00
vietc.toml fix: start_enabled=true by default, log daemon to file instead of /dev/null 2026-06-26 09:09:04 +07:00

Platform Rust License Version


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:

  1. The keystroke is appended to keystroke_history
  2. A brand new Engine is created
  3. The entire history is replayed through it
  4. The engine's buffer is the correct screen output
  5. 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 â tantân
aw ă tantăn
ee ê menmên
oo ô to
ow ơ to
uw ư tu
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 (tranaftrần) — engine scans backward to find the vowel
Smart Clusters uoươ, ươ + o, 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 kokhông, dcđược, custom shortcuts
Casing Preservation SATSSÁT, SaaSả — 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

./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