Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/khaphanspace/gonhanh.org/llms.txt

Use this file to discover all available pages before exploring further.

Overview

Gõ Nhanh is a cross-platform Vietnamese input method application with a validation-first, pattern-based architecture. The system consists of platform-specific UI layers (macOS, Windows) communicating with a shared Rust core engine via FFI.

High-Level Architecture

┌──────────────────────────────────────────┐   ┌──────────────────────────────────────────┐
│         macOS Application                │   │      Windows Application                 │
│                                          │   │                                          │
│  ┌────────────────────────────────┐     │   │  ┌────────────────────────────────┐     │
│  │     SwiftUI Menu Bar           │     │   │  │   WPF System Tray UI           │     │
│  │  • Input method selector       │     │   │  │  • Input method selector       │     │
│  │  • Enable/disable toggle       │     │   │  │  • Enable/disable toggle       │     │
│  │  • Settings, About, Update     │     │   │  │  • Settings, About, Update     │     │
│  └────────────┬────────────────────┘     │   │  └────────────┬────────────────────┘     │
│               │                          │   │               │                          │
│  ┌────────────▼────────────────────┐     │   │  ┌────────────▼────────────────────┐     │
│  │ CGEventTap Keyboard Hook        │     │   │  │ SetWindowsHookEx Keyboard Hook  │     │
│  │ • Intercepts keyDown events     │     │   │  │ • Intercepts WH_KEYBOARD_LL     │     │
│  │ • Smart text replacement        │     │   │  │ • SendInput for text            │     │
│  └────────────┬────────────────────┘     │   │  └────────────┬────────────────────┘     │
│               │                          │   │               │                          │
│  ┌────────────▼────────────────────┐     │   │  ┌────────────▼────────────────────┐     │
│  │    RustBridge (FFI Layer)       │     │   │  │   RustBridge.cs (P/Invoke)     │     │
│  │  • C ABI function calls         │     │   │  │  • P/Invoke DLL function calls  │     │
│  │  • Pointer safety handling      │     │   │  │  • UTF-32 interop               │     │
│  └────────────┬────────────────────┘     │   │  └────────────┬────────────────────┘     │
└───────────────┼──────────────────────────┘   └───────────────┼──────────────────────────┘
                │                                               │
                └───────────────────┬──────────────────────────┘

                         extern "C" / P/Invoke

         ┌─────────────────────────────────────────────┐
         │     Rust Core Engine (Platform-Agnostic)   │
         │     7-Stage Validation-First Pipeline       │
         └─────────────────────────────────────────────┘

Core Components

Platform Layer

// Menu bar interface for input method control
MenuBarController.init()
  ├─ Create status bar icon
  ├─ Load settings from UserDefaults
  ├─ If accessibility trusted: startEngine()
  └─ Otherwise: show permission prompt
Accessibility Permission Required:
  • API: AXIsProcessTrusted() checks permission
  • User must add app to: System Settings → Privacy & Security → Accessibility
  • App restart required after granting permission

FFI Interface

The Rust core engine exposes a C-compatible ABI for cross-platform integration:
core/src/lib.rs
/// Initialize engine (call once)
#[no_mangle]
pub extern "C" fn ime_init()

/// Process keystroke
#[no_mangle]
pub extern "C" fn ime_key(key: u16, caps: bool, ctrl: bool) -> *mut Result

/// Process keystroke with extended parameters
#[no_mangle]
pub extern "C" fn ime_key_ext(key: u16, caps: bool, ctrl: bool, shift: bool) -> *mut Result

/// Set input method (0=Telex, 1=VNI)
#[no_mangle]
pub extern "C" fn ime_method(method: u8)

/// Enable/disable engine
#[no_mangle]
pub extern "C" fn ime_enabled(enabled: bool)

/// Clear buffer
#[no_mangle]
pub extern "C" fn ime_clear()

/// Free result
#[no_mangle]
pub unsafe extern "C" fn ime_free(result: *mut Result)
Result Structure:
typedef struct {
    uint32_t chars[32];      // UTF-32 output characters
    uint8_t action;          // 0=None, 1=Send, 2=Restore
    uint8_t backspace;       // Number of chars to delete
    uint8_t count;           // Number of valid chars
    uint8_t _pad;            // Padding for alignment
} ImeResult;

Rust Core Engine

Platform-agnostic Vietnamese input processing:
pub struct Engine {
    buf: Buffer,              // Circular buffer (256 chars max)
    method: u8,               // 0=Telex, 1=VNI
    enabled: bool,
    last_transform: Option<Transform>,
    shortcuts: ShortcutTable,
    raw_input: Vec<(u16, bool, bool)>,
    // ... configuration flags
}
Key Features:
  • Thread-safe via Mutex<Option<Engine>>
  • 7-stage processing pipeline
  • Validation-first approach
  • Pattern-based transformation
  • User-defined shortcuts

Data Flow

Keystroke Processing Flow

User types key

CGEventTap/SetWindowsHookEx callback fires

Extract keycode + modifier flags

Check global hotkey (Ctrl+Space) → Toggle Vietnamese

Call RustBridge.processKey()
   ├─ Call ime_key(keycode, caps, ctrl)
   ├─ Receive ImeResult
   ├─ Extract UTF-32 chars → Character array
   └─ Return (backspaceCount, chars) tuple

If transformation:
   ├─ Send backspaces (CGEvent/SendInput)
   ├─ Send Unicode replacement
   └─ Consume original key (return nil/suppress)

Else: Pass through (return unmodified event)

Visible to user as transformed or original text

Example: Typing “á” in Telex

1

Type 'a'

├─ CGEventTap captures keyDown
├─ RustBridge.processKey(keyCode=0x00, caps=false, ctrl=false)
├─ Rust: ime_key() called
├─ Engine:
│  ├─ Append 'a' to buffer
│  ├─ Validate: "a" is valid (vowel alone)
│  ├─ No transform yet (single char, waiting for next)
│  └─ Return Action::None (pass through)
├─ Swift: No action, let 'a' appear naturally
└─ Output: User sees 'a' typed
2

Type 's' (sắc mark)

├─ CGEventTap captures keyDown
├─ RustBridge.processKey(keyCode=0x01, caps=false, ctrl=false)
├─ Rust: ime_key() called
├─ Engine:
│  ├─ Check buffer context: "a" + "s" → sắc mark
│  ├─ Validation: "á" is valid Vietnamese vowel
│  ├─ Transform: Apply sắc mark to 'a' → 'á'
│  ├─ Check shortcuts: No expansion needed
│  ├─ Return Action::Send {
│  │    backspace: 1,  // Delete 'a'
│  │    chars: ['á']   // Insert 'á'
│  └─ }
├─ Swift:
│  ├─ Send 1 backspace (delete 'a')
│  ├─ Send 'á' (via Unicode keyboard event)
│  └─ 's' keystroke consumed (not passed through)
└─ Output: User sees 'á' (exactly 1 character)
Result: “á” displayed instead of “as”
Latency: ~0.2-0.5ms total (Rust engine: less than 0.1ms)

Text Replacement Strategies

macOS: Backspace vs Selection

Gõ Nhanh uses accessibility-based detection to choose the optimal replacement method:
MethodUse CaseHow It Works
BackspaceBody text (90% of cases)Send BS chars, then insert replacement
SelectionAddress bars, autocomplete fieldsSend Shift+Left to select, then insert
Detection Strategy:
func getReplacementMethod() -> ReplacementMethod {
    guard let info = getFocusedElementInfo() else {
        return .backspace // Default
    }

    // Rule 1: ComboBox = address bar, dropdown
    if info.role == "AXComboBox" {
        return .selection
    }

    // Rule 2: Search field with autocomplete
    if info.role == "AXTextField" && info.subrole == "AXSearchField" {
        return .selection
    }

    // Rule 3: JetBrains IDEs
    if info.bundleId.hasPrefix("com.jetbrains") {
        return .selection
    }

    // Rule 4: Microsoft Excel
    if info.bundleId == "com.microsoft.Excel" {
        return .selection
    }

    // Default: Backspace (fast, no flicker)
    return .backspace
}

App Compatibility

AppBundle IDBody TextAddress BarSearch Box
Chromecom.google.Chrome✅ Backspace❌ Selection⚠️ Selection
Safaricom.apple.Safari✅ Backspace❌ Selection⚠️ Selection
Firefoxorg.mozilla.firefox✅ Backspace❌ Selection⚠️ Selection
AppBundle IDIssueMethod
VS Codecom.microsoft.VSCodeNoneBackspace
Xcodecom.apple.dt.XcodeNone (native)Backspace
Android Studiocom.google.android.studioAutocomplete popupSelection
IntelliJcom.jetbrains.intellijAutocompleteSelection

Performance Characteristics

Latency Budget

ComponentTimeNotes
CGEventTap callback~50μsSystem kernel time
Rust ime_key()~100-200μsEngine processing
Swift RustBridge~50μsFFI overhead + result conversion
CGEvent sending~100-200μsPosting to event tap
Total~300-500μssub-1ms requirement met

Memory Profile

ComponentSizeNotes
Rust engine (static)~150KBTables + code
Swift runtime~4.5MBStandard SwiftUI overhead
Buffer (256 chars)~200BCircular buffer per engine instance
Total~5MBMeets requirement

Component Interaction Sequence

Architecture Decisions

  • Performance: Sub-millisecond processing required
  • Safety: No memory leaks or crashes in critical path
  • Cross-platform: Single codebase for macOS, Windows, Linux
  • FFI: Clean C ABI for platform integration
  • Protection: Prevents false transformations on English words
  • Accuracy: Only transforms valid Vietnamese syllables
  • User experience: No accidental modifications to code, URLs, etc.
  • Clarity: Each stage has single responsibility
  • Testability: Isolated stages easy to test
  • Extensibility: New features add stages without breaking existing

Last Updated: 2025-12-14
Architecture Version: 2.0 (Validation-First, Cross-Platform)
Platforms: macOS (CGEventTap), Windows (SetWindowsHookEx), Linux (Fcitx5 - beta)