A modern Vietnamese Input Method Engine (IME) for Linux with direct Unicode input—no pre-edit buffer, no underlines.
Find a file
Khoa Vo 88d39b4475
Some checks are pending
Build & Release / Build & test (push) Waiting to run
Build & Release / Build .deb (push) Blocked by required conditions
release: v0.1.6 — uinput-first injection, window-switch fix, Telex disabled
- uinput injection is now primary on X11 (XTest fallback)
- X11 XTest keycode offset +8 fixed for all send_keycode paths
- Window switch detection on every keystroke (no more gap > 100ms guard)
- Telex greyed out in tray with '(next version)' label
- Flatpak and AppImage removed; only .deb packaging
- All Cargo.toml versions bumped to 0.1.6
2026-06-29 16:07:15 +07:00
.github/workflows release: v0.1.6 — uinput-first injection, window-switch fix, Telex disabled 2026-06-29 16:07:15 +07:00
cli release: v0.1.6 — uinput-first injection, window-switch fix, Telex disabled 2026-06-29 16:07:15 +07:00
daemon release: v0.1.6 — uinput-first injection, window-switch fix, Telex disabled 2026-06-29 16:07:15 +07:00
engine release: v0.1.6 — uinput-first injection, window-switch fix, Telex disabled 2026-06-29 16:07:15 +07:00
packaging release: v0.1.6 — uinput-first injection, window-switch fix, Telex disabled 2026-06-29 16:07:15 +07:00
protocol release: v0.1.6 — uinput-first injection, window-switch fix, Telex disabled 2026-06-29 16:07:15 +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 release: v0.1.6 — uinput-first injection, window-switch fix, Telex disabled 2026-06-29 16:07:15 +07:00
uinputd release: v0.1.6 — uinput-first injection, window-switch fix, Telex disabled 2026-06-29 16:07:15 +07:00
.gitignore release: v0.1.5 — Event Sourcing, Flatpak build fixes, icons 2026-06-28 21:20:19 +07:00
Cargo.toml fix: X11 key lookup, bamboo engine port, uinput injection overhaul 2026-06-26 15:20:03 +07:00
CHANGELOG.md release: v0.1.6 — uinput-first injection, window-switch fix, Telex disabled 2026-06-29 16:07:15 +07:00
LICENSE Viet+ v0.1.0 - Vietnamese Input Method for Linux 2026-06-24 10:13:10 +07:00
Makefile release: v0.1.6 — uinput-first injection, window-switch fix, Telex disabled 2026-06-29 16:07:15 +07:00
README.md release: v0.1.6 — uinput-first injection, window-switch fix, Telex disabled 2026-06-29 16:07:15 +07:00
RELEASE_CHECKLIST.md release: v0.1.6 — uinput-first injection, window-switch fix, Telex disabled 2026-06-29 16:07:15 +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 Tests Event Sourcing


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   │  │ Window switch    │   │
│  │ (libevdev)  │  │ (XRecord)    │  │ detection (250ms)│   │
│  └─────────────┘  └──────────────┘  └──────────────────┘   │
└──────────────────────────────────────────────────────────────┘
       │
       ▼
┌──────────────────────────────────────────────────────────────┐
│  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 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                                      │
│                                                              │
│  Primary: uinput injection (evdev keycodes, correct on all   │
│    display servers — routed through libinput on modern X11)  │
│  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: X11 XTest injection (X11 keycodes = evdev + 8)    │
└──────────────────────────────────────────────────────────────┘
       │
       ▼
   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:

  1. The keystroke is appended as an InputEvent to the EventStore
  2. A brand new Engine is created
  3. The entire event history is replayed through it via Engine::replay_events()
  4. The engine's buffer is the correct screen output
  5. 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-safeEventStore::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      # 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 (fallback)
│   ├── uinput_monitor.rs    # /dev/uinput injection (primary)
│   ├── 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/               # .deb packaging scripts
└── vietc.toml               # Default configuration

Component Interaction

┌─────────────────────────────────────────────────────────────┐
│                      vietc-tray                             │
│  (System tray icon, daemon launcher)                        │
└───────────────────────┬─────────────────────────────────────┘
                        │ starts
                        ▼
┌─────────────────────────────────────────────────────────────┐
│                      vietc-daemon                            │
│                                                              │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────┐  │
│  │ Config       │  │ App State    │  │ Display          │  │
│  │ (hot reload) │  │ (per-app)    │  │ (X11/Wayland)    │  │
│  └──────┬───────┘  └──────┬───────┘  └────────┬─────────┘  │
│         │                 │                    │             │
│         └─────────────────┼────────────────────┘             │
│                           │                                  │
│                    ┌──────▼──────┐                           │
│                    │ Event Loop  │                           │
│                    │             │                           │
│                    │ evdev: grab │                           │
│                    │ keyboard    │                           │
│                    │             │                           │
│                    │ Process     │                           │
│                    │ keystroke   │                           │
│                    │             │                           │
│                    │ Replay all  │                           │
│                    │ history     │                           │
│                    │             │                           │
│                    │ Inject      │                           │
│                    │ diff        │                           │
│                    └─────────────┘                           │
│                                                              │
│  ┌────────────────────────────────────────────────────────┐ │
│  │                   vietc-engine                         │ │
│  │  VniEngine / EnglishDict / Spelling                    │ │
│  └────────────────────────────────────────────────────────┘ │
│                                                              │
│  ┌────────────────────────────────────────────────────────┐ │
│  │               vietc-protocol                           │ │
│  │  UinputInjector / X11Injector / X11Capture / Wayland  │ │
│  └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

Input Methods

VNI (default, Telex coming in next version)

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: nguye6n4nguyễ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 injection
Bamboo Engine Transformation model ported from bamboo-core — composition, marks, tones, flexible backtracking
Flexible Backtrack Type tone/modifier at end of syllable (tran5trạn). Scans up to 5 chars backward
Smart Clusters uoươ with backtrack (chuong7chương)
Tone Placement Correct tone positioning for all Vietnamese diphthongs (io→gió, uâ→xuất, yê→nguyễn)
Macro Expansion kokhông, dcđược, custom shortcuts
Casing Preservation TieengsTiếng, TIEENGSTIẾ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 Active window ID verified on every keystroke — Alt+Tab instantly clears engine state. No stale composition across apps
CPU Priority Pins daemon to P-cores (0-3) + nice(-10) for low-latency input
Uinput Injection Uses /dev/uinput for reliable keyboard injection without X11 dependency. Falls back to XTest on systems without uinput access

Installation

System tray icon + daemon + desktop entry. Requires user to be in the input group for keyboard capture.

# Install
sudo dpkg -i vietc_0.1.6-1_amd64.deb

# Log out and log back in (for input group membership to take effect)
# Then launch "Viet+" from your application menu

The post-install script will:

  • Kill any running tray/daemon processes
  • Remove stale binaries from /usr/local/bin/
  • Add your user to the input group
  • Prompt you to log out and back in

Build from Source

git clone https://github.com/vndangkhoa/vietc.git
cd vietc
make deb
sudo dpkg -i packaging/deb/vietc_0.1.6-1_amd64.deb

Requires Rust toolchain, pkg-config, libx11-dev, libxtst-dev, libevdev-dev. See packaging/deb/build-deb.sh for details.


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 = true       # Vietnamese 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