A modern Vietnamese Input Method Engine (IME) for Linux with direct Unicode input—no pre-edit buffer, no underlines.
Find a file
Khoa Vo 9e073714f1
Some checks are pending
Build & Release / Build & test (push) Waiting to run
Build & Release / Build .deb (push) Blocked by required conditions
fix: non-blocking evdev poll (200ms timeout) so Ctrl+C ungrabs reliably
2026-07-01 15:53:25 +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.7 — password detection, Telex enabled, GNOME Wayland support 2026-07-01 11:00:11 +07:00
daemon fix: non-blocking evdev poll (200ms timeout) so Ctrl+C ungrabs reliably 2026-07-01 15:53:25 +07:00
engine release: v0.1.7 — password detection, Telex enabled, GNOME Wayland support 2026-07-01 11:00:11 +07:00
packaging add installation and packaging scripts 2026-06-29 20:45:48 +07:00
protocol fix: wl-copy --paste-once for fast clipboard on Wayland/GNOME 2026-07-01 12:41:56 +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.7 — password detection, Telex enabled, GNOME Wayland support 2026-07-01 11:00:11 +07:00
uinputd release: v0.1.7 — password detection, Telex enabled, GNOME Wayland support 2026-07-01 11:00:11 +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 docs: update CHANGELOG with recent fixes, update test count 2026-07-01 12:11:14 +07:00
install.sh fix: improve Ubuntu and derivatives support in install scripts 2026-07-01 08:12:47 +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 docs: update CHANGELOG with recent fixes, update test count 2026-07-01 12:11:14 +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
uninstall.sh fix: improve Ubuntu and derivatives support in install scripts 2026-07-01 08:12:47 +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                       │
  │  Ctrl+Shift → toggle VNI/Telex input method                  │
  │  Password detected → auto-disable Vietnamese                 │
  │  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                                      │
│                                                              │
│  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 VN/EN memory + password detection
│   ├── password_detector.rs # AT-SPI2 D-Bus password field detection
│   └── 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
│   └── tray.rs              # Tray with VN/TLX/EN mode display
│
├── 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

Both VNI and Telex are fully supported. Switch between them via:

  • Ctrl+Shift hotkey (toggle at runtime)
  • System tray menu: "Input Method > Telex / VNI"
  • Config file: input_method = "vni" or "telex"

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đ

Telex

Key Result Example
s á (sắc) asá
f à (huyền) afà
r ả (hỏi) ar
x ã (ngã) axã
j ạ (nặng) aj
aa â aaâ
ee ê eeê
oo ô ooô
ow ơ owơ
aw ă awă
uw ư uwư
dd đ ddđ
w ươ (uo cluster) chuongwchương

Flexible typing: type the full syllable, then add marks/tone keys at the end. Examples: tieengstiếng, nguyeexnnguyễn, chafochào. 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
Password Auto-Detection AT-SPI2 + window-class + window-title — automatically disables Vietnamese when typing into password fields
Method Toggle Ctrl+Shift switches between VNI and Telex at runtime; tray icon shows current mode (VN/TLX/EN)
GNOME/Wayland Support Native GNOME Shell D-Bus integration for window detection, app memory, and password detection on Wayland
VNI & Telex Both input methods fully supported, switchable at runtime

Installation

Single Command (from Source)

Depending on which repository you prefer to clone from, you can use one of the following commands to install or update Viet+ in a single step:

Install / Update:

git clone https://github.com/vndangkhoa/vietc.git /tmp/vietc && cd /tmp/vietc && sudo ./install.sh

Uninstall:

curl -sSL https://raw.githubusercontent.com/vndangkhoa/vietc/main/uninstall.sh | sudo bash

From Forgejo (Self-Hosted)

Install / Update:

git clone https://git.khoavo.myds.me/vndangkhoa/vietc.git /tmp/vietc && cd /tmp/vietc && sudo ./install.sh

Uninstall:

curl -sSL https://git.khoavo.myds.me/vndangkhoa/vietc/raw/branch/main/uninstall.sh | sudo bash

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

# Install
sudo dpkg -i vietc_0.1.7-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 VN/EN
toggle_method_key = "shift" # Ctrl+Shift to toggle VNI/Telex
start_enabled = true       # Vietnamese by default
grab = true                # grab keyboard (evdev)

[auto_restore]
enabled = true
trigger_keys = ["space", "escape"]

[password_detection]
enabled = true
check_atspi2 = true        # AT-SPI2 accessibility bus detection
check_window_title = true
title_keywords = ["password", "passphrase", "secret", "mật khẩu", "sudo"]
password_apps = ["pinentry", "pinentry-gtk-2", "pinentry-qt",
  "lxqt-sudo", "kdesudo", "gksudo",
  "polkit-gnome-authentication-agent-1",
  "kwallet", "gnome-keyring", "ssh-askpass"]

[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