System-wide locally running AI text autocomplete for Linux.
Inline ghost-text completions powered by a local LLM, delivered through the Wayland-native input method framework. No cloud, no telemetry, just a clean and simple Fcitx5 engine that works in every text field on your desktop.
- Summary
- Introduction
- Features
- Installation
- Configuration
- Dependencies
- Systems & Technical Details
- Project Structure
- Application Compatibility
- Roadmap
- Acknowledgements
- License
XType brings Cotypist-style inline AI autocomplete to Linux. This project is vibe coded by one developer, and basically a personal project. Dont expect perfect support or production quality :D**
As you type, a small local language model continues your sentence in greyed-out
ghost text directly inside whatever app you're using — your editor, your browser,
your office suite. Press Tab to accept the next word, Shift+Tab to accept the
whole suggestion, Esc to dismiss it. Everything happens on-device, on your CPU
or GPU, with the model held in roughly 1–2 GB of RAM and a target
time-to-first-token under 200 ms.
It is implemented as a native Fcitx5 input method engine so it works across GTK, Qt, Electron and SDL applications without ever touching the keyboard, the X server, or the network.
On macOS, Cotypist by Daniel Gräfe (Accelerated Thought GmbH) demonstrated how delightful system-wide AI autocomplete can feel when it's tightly integrated with the OS.
Input on Linux is fragmented across X11 and Wayland, multiple toolkits (GTK,
Qt, Electron, SDL), and shifting protocols. Wayland deliberately blocks global
key interception as a security boundary; xdotool does not work; AT-SPI2 is
inconsistent across toolkits.
The clean answer is the one Wayland itself gives you: become the input
method. Every modern toolkit on Wayland speaks the text-input protocol, and
input methods are first-class citizens with full rights to render preedit
text (ghost text) inline in the focused field. That is the opening XType walks
through.
- Inline ghost text rendered directly in the application's own text field — no popup window, no overlay, no z-order race with your tiling WM.
- Local-only inference via Ollama, defaulting to Qwen 2.5 1.5B quantised to Q4_K_M.
- Streaming with cancel-on-new-keystroke, so a stale request never paints over your fresh typing.
- Word-by-word acceptance (
Tab) or full-suggestion acceptance (Shift+Tab). Accept key is configurable: Tab, Enter, or →. - Sub-200 ms time-to-first-token target, measured with the included benchmark script.
- Per-app blocklist — terminals (Konsole, Alacritty), password managers (KeePassXC, 1Password), and any app you nominate are skipped automatically.
- Browser-aware: dedicated handling for the GTK4/Firefox per-keystroke activate/deactivate cycle (so Zen Browser actually accumulates context) and for Chromium's text-input-v3 commit ordering (so ghost text doesn't leak into the field on focus loss).
- Debounced at 220 ms by default so Ollama isn't spammed mid-burst.
- Anti-loop chat format — uses
assistant-prefill messaging on Ollama's/api/chatendpoint so the model continues your text instead of replying to it. xtype-settingsQt6/QML app — full settings interface for all engine options, accessible from KDE application menus; writes~/.config/xtype/config.tomland shows a reload banner when changes need to be applied.- Personalisation — opt-in writing corpus (
~/.local/share/xtype/corpus.txt), style profile extraction, voice-match strength slider (0–100), and a forget-after-days rolling window to keep the profile fresh automatically. - User voice profile — free-text description (≤ 500 chars), tone selector (Default / Casual / Professional / Technical / Concise), and a per-phrase avoid list — all injected into the system prompt.
- Per-app overrides — independently configure suggestion length, mode, debounce, and a prompt addendum per application.
- Live observability — engine writes
metrics.jsonandrecent_events.jsonafter every inference; the sidebar shows acceptance rate, latency p50/p95, and a recent-events log without a page reload. - Privacy by design — no telemetry, no cloud, no analytics, no auto-update ping. Password managers are permanently hard-blocked from corpus collection; AI suggestions never enter the corpus.
XType is currently developed on CachyOS / Arch Linux with KDE Plasma on Wayland. Other distros and desktops should work but are not yet validated.
sudo pacman -S \
fcitx5 fcitx5-configtool fcitx5-qt fcitx5-gtk fcitx5-breeze \
cmake ninja gcc pkgconf curl \
ollamaollama pull qwen2.5:1.5b # production
ollama pull qwen2.5:0.5b # smaller dev model (optional)Make sure the Ollama daemon is running:
systemctl --user enable --now ollamagit clone https://github.com/K-Sugar/XType.git
cd XType/fcitx5-engine
cmake -B build -G Ninja -DFCITX_INSTALL_USE_FCITX_SYS_PATHS=ON
ninja -C build
sudo ninja -C build installThis installs:
/usr/lib/fcitx5/libxtype-fcitx5.so/usr/share/fcitx5/addon/xtype.conf/usr/share/fcitx5/inputmethod/xtype.conf
cmake -B settings/qt6-app/build -G Ninja -S settings/qt6-app
ninja -C settings/qt6-app/build
sudo ninja -C settings/qt6-app/build install # installs xtype-settings + .desktopOr run it directly without installing:
./settings/qt6-app/build/xtype-settings~/.config/environment.d/input-method.conf:
QT_IM_MODULE=fcitx
GTK_IM_MODULE=fcitx
XMODIFIERS=@im=fcitxKDE Plasma on Wayland requires that Fcitx5 be launched by KWin — not as a standalone autostart daemon. Open:
System Settings → Input & Output → Keyboard → Virtual Keyboard → Fcitx5
Then disable any IBus autostart entry to avoid conflicts, log out, and log back
in. Verify with fcitx5-configtool that XType appears in your enabled
input methods.
Recommended: use the xtype-settings GUI app (see settings/qt6-app/README.md).
It writes ~/.config/xtype/config.toml automatically and shows a reload banner
when changes need to be applied.
CLI fallback: edit ~/.config/xtype/config.toml directly (created on first launch
with sane defaults). All keys are optional.
[inference]
model = "qwen2.5:1.5b"
ollama_host = "http://localhost:11434"
debounce_ms = 220
min_context_chars = 10
context_window = 150
num_predict = 30
temperature = 0.3
top_p = 0.9
stop_tokens = [".", "!", "?", "\n"]
[behaviour]
engine_enabled = true
accept_full_key = "tab" # "tab" | "enter" | "right"
partial_accept = true
esc_dismisses = true
blocklist_apps = ["konsole", "alacritty", "keepassxc", "1password"]
[learning]
enabled = false # opt-in
corpus_path = "~/.local/share/xtype/corpus.txt"
voice_strength = 50 # 0–100
forget_after_days = 0 # 0 = disabled
[user_prompt]
description = "" # ≤ 500 chars; injected into system prompt
tone = "" # "casual" | "professional" | "technical" | "concise"
avoid_phrases = []
[apps.firefox] # per-app override example
num_predict = 20
mode = "Default"| Key | Effect |
|---|---|
model |
Any model installed in Ollama. qwen2.5:0.5b is faster, 1.5b is sharper. |
debounce_ms |
Idle time before a request is fired. Lower = snappier, higher = less load. |
accept_full_key |
Which key accepts the full suggestion: "tab", "enter", or "right". |
partial_accept |
When true, the accept key commits one word at a time; Shift+key commits all. |
min_context_chars |
Minimum buffered characters before the engine asks for a suggestion. |
context_window |
Sliding-window size (chars) handed to the model as prefill. |
blocklist_apps |
Case-insensitive component match against ic->program(). |
learning.voice_strength |
0 = no corpus examples in prompt; 100 = all exemplars included; proportional. |
learning.forget_after_days |
Prune corpus entries older than N days. 0 disables pruning. |
user_prompt.description |
Free-text description of yourself injected into the system prompt (≤ 500 chars). |
Logs are written to ~/.local/share/xtype/fcitx5.log (engine debug) and
~/.local/share/xtype/inference.log (inference requests). Both are viewable in
the LogTail panel in xtype-settings.
| Dependency | Version | Why |
|---|---|---|
| Linux | any modern | Tested on kernel 6.19 (CachyOS) |
| Wayland | — | Required for KDE Plasma; X11 works via XWayland |
| Fcitx5 | ≥ 5.0 | Production input method |
| Ollama | ≥ 0.1.30 | Local LLM runtime |
| libcurl | ≥ 7.80 | HTTP streaming to Ollama |
| Qwen 2.5 | 1.5B Q4_K_M | Default model (~1 GB on disk, ~1.5 GB resident) |
- CMake ≥ 3.19
- Ninja
- GCC ≥ 13 or Clang ≥ 16 (C++20)
- pkgconf
- Catch2 v3 (only when
-DBUILD_TESTS=ON)
- Catch2 v3 — C++ unit tests (engine + settings app)
┌────────────────────────────────────────────────────────────────────┐
│ Application (Kate, Firefox, …) │
│ │
│ ▲ keystrokes ▼ preedit / commit │
└───┬──────────────────────────────────────────────────────────▲─────┘
│ │
│ Wayland text-input-v2 / v3 │
│ │
┌───▼──────────────────────────────────────────────────────────┴─────┐
│ Fcitx5 daemon │
└───┬──────────────────────────────────────────────────────────▲─────┘
│ │
│ C++ addon API │
│ │
┌───▼──────────────────────────────────────────────────────────┴─────┐
│ XTypeEngine │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌────────────────────────┐ │
│ │ KeyHandler │──▶│ Debouncer │──▶│ ContextBuffer (deque) │ │
│ └─────────────┘ └──────┬───────┘ └────────────┬───────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ InferenceClient │◀─────│ Prompt builder │ │
│ │ (libcurl, std::thread) │ (chat / prefill)│ │
│ └────────┬────────┘ └─────────────────┘ │
└───────────────────────────┼────────────────────────────────────────┘
│ HTTP POST /api/chat (streaming)
▼
┌─────────────────────┐
│ Ollama (local) │
│ Qwen 2.5 1.5B │
└─────────────────────┘
Wayland's text-input protocol is ubiquitous: GTK, Qt, Electron, and SDL all
implement it. Input methods are explicitly granted the right to display
preedit text — visually distinct, not-yet-committed text that lives inside
the focused widget. That is exactly the surface ghost-text autocomplete needs,
and it ships universally.
The alternative paths were considered and rejected:
- AT-SPI2 + key injection — inconsistent on Wayland, no inline rendering.
xdotool/ Uinput — broken on Wayland, requires root or input groups, cannot render anything.- Per-app extensions — does not scale to "every app".
David Edmundson's
input-method-playground
validated this exact architecture on KDE Wayland.
| Event | Action |
|---|---|
| Printable char | Pass through, append to buffer, dismiss old, debounce inference |
Tab (suggestion active) |
Commit next word; update preedit with remainder |
Shift+Tab (suggestion active) |
Commit entire suggestion; clear preedit |
Esc (suggestion active) |
Dismiss; clear preedit |
Backspace (suggestion active) |
Dismiss only |
Backspace (no suggestion) |
Pop last char from buffer; pass through |
Enter (suggestion active) |
Accept full suggestion if accept_full_key = "enter"; otherwise dismiss and pass through |
Ctrl/Alt/Super + key |
Always pass through |
| Key release | Always ignored |
| Focus out | Discard preedit (commitString("") + clearPreedit) |
| Focus in / reset | Clear preedit, reset context |
Critically, focus-out must not commit ghost text. The suggestion is the model's, not the user's; committing it would leak unaccepted text into the document on every Alt-Tab. The engine sends an empty commit followed by an empty preedit before Fcitx5 finishes its deactivation sequence — necessary because Chromium's text-input-v3 implementation otherwise commits the preedit before the engine's deactivation callback can run.
- Background
std::threadwith anstd::atomicgeneration counter for cancellation. - Streaming NDJSON parser; tokens delivered to a callback as they arrive.
- Marshalled back to the main thread via Fcitx5's
eventDispatcher().schedule()— never call IM APIs off the main thread. - Default Ollama options:
num_predict=30,temperature=0.3,top_p=0.9,stop=[".", "!", "?", "\n"]. - Anti-loop strategy: the context is sent as an
assistant-role message on/api/chat, with a small system prompt instructing the model to continue rather than respond. A literal[CURSOR]marker was tried and abandoned — prefill achieves the same effect without a magic token.
A std::deque<char> (500-char sliding window) tracks the last context_window
characters typed. It exposes a small state machine:
appendChar() ─┐
backspace() ─┼─▶ auto-dismiss any active suggestion
setSuggestion(text) ─┘
acceptNextWord() ─▶ returns next word + space, advances index, appends to buffer
acceptAll() ─▶ returns full remaining suggestion
dismiss() ─▶ clears suggestion only
contextText() ─▶ entire buffer as std::string
The implementation is covered by Catch2 unit tests.
- Q4_K_M Qwen 2.5 1.5B sits around 1.5 GB resident on CPU and produces a first token in 70–180 ms on a modern desktop with Ollama's default backends.
- The 0.5B variant is roughly 2× faster but noticeably less coherent — kept as a development convenience.
debounce_msdefaults to 220 ms (intentionally a touch above typical burst-typing intervals) to avoid issuing requests that will be cancelled mid-flight.
XType/
├── Summary.md High-level project context & rationale
│
├── fcitx5-engine/ Production Fcitx5 addon (C++20)
│ ├── CMakeLists.txt
│ ├── src/
│ │ ├── xtype.{h,cpp} InputMethodEngineV2 — key handler, lifecycle
│ │ ├── inference_client.{h,cpp} libcurl streaming client, system prompt mgmt
│ │ ├── context_buffer.{h,cpp} 500-char rolling context window
│ │ ├── config.h Config structs (InferenceConfig, etc.)
│ │ ├── config_loader.{h,cpp} TOML → config struct parser
│ │ ├── corpus_collector.{h,cpp} Opt-in background corpus writer
│ │ ├── style_profile.{h,cpp} Exemplar extraction + JSON round-trip
│ │ ├── prompt_builder.{h,cpp} System prompt assembly with budget cap
│ │ ├── phrase_blocklist.h Case-insensitive phrase filter
│ │ ├── engine_metrics.h Atomic latency / acceptance counters
│ │ ├── recent_events.h Ring buffer of last N inference events
│ │ └── path_utils.{h,cpp} ~ expansion helper
│ ├── data/
│ │ ├── xtype.conf.in Fcitx5 addon descriptor
│ │ └── xtype-im.conf Input method registration
│ └── tests/ Catch2 unit tests
│
├── settings/qt6-app/ Qt6/QML settings interface (xtype-settings)
│ ├── CMakeLists.txt
│ ├── src/
│ │ ├── config_store.{h,cpp} Single TOML source of truth for QML
│ │ ├── engine_probe.{h,cpp} Polls metrics.json / recent_events.json
│ │ ├── corpus_stats.{h,cpp} CorpusStats QML context property
│ │ ├── style_profile_model.{h,cpp} Exposes exemplars to QML
│ │ ├── log_tail.{h,cpp} Tails fcitx5.log / inference.log
│ │ ├── reloader.{h,cpp} Triggers fcitx5-remote -r
│ │ └── ollama_client.{h,cpp} Fetches available model list
│ └── qml/
│ ├── pages/ PageGeneral, PagePersonalisation, PageBlockList,
│ │ PagePerApp, PageModel, PageAbout
│ └── primitives/ Shared UI components (XSlider, XToggle, etc.)
│
├── shared/prompt_templates/ Prompts shared between engines
├── scripts/benchmark_ollama.py TTFT benchmark harness (p50/p95/p99)
├── packaging/ PKGBUILD, systemd service, .desktop file
└── docs/ Integration test matrices, per-app mode docs
Status from the integration test matrix on KDE Plasma Wayland.
| Application | Toolkit | Wayland Protocol | Status |
|---|---|---|---|
| Kate | Qt 6 | text-input-v2 | Full preedit & word-by-word accept |
| Firefox / Zen | GTK 3 / 4 | text-input-v3 | Working (per-keystroke cycle handled) |
| Chromium | Blink | text-input-v1/v3 | Needs --wayland-text-input-version=3 |
| LibreOffice | GTK 3 | text-input-v3 | Working |
| Electron apps | Chromium | text-input-v1 | Needs ELECTRON_OZONE_PLATFORM_HINT=wayland |
| Konsole | Qt 6 | text-input-v2 | Blocklisted (terminal — preedit glitches) |
| Alacritty | OpenGL | text-input-v3 | Blocklisted |
| KeePassXC | Qt 6 | text-input-v2 | Blocklisted (sensitive input) |
- Phase 0 — Async Ollama client, ContextBuffer, Debouncer, TTFT benchmark harness
- Fcitx5 engine — CMake C++20 addon, libcurl streaming client, engine core, KDE Plasma Wayland integration matrix
- Browser compatibility hardening — Zen GTK4 per-keystroke cycle, Chromium text-input-v3 commit ordering
- Anti-loop inference —
qwen2.5:1.5bupgrade with assistant-prefill chat format - Personalisation engine — opt-in corpus collector, style profile extraction (representative exemplars, privacy filtering), dynamic system-prompt assembly capped at 2000 chars, 5-minute background refresh
- User voice profile —
description,tone,avoid_phrases, voice-match strength slider, forget-after-days rolling window - Per-app overrides —
enabled,model,debounce_ms,num_predict,mode,prompt_addendumper application - Qt6/QML settings app (
xtype-settings) — all six pages wired to the engine viaconfig.toml; sidebar live metrics; reload banner - Engine ↔ UI observability —
metrics.json,recent_events.json,inference.log, LogTail panel
- Model presets — hardware-tier presets (Low / Balanced / High / Enthusiast) with RAM-based auto-selection and Ollama health-check fallback
- PKGBUILD + AUR — packaging, systemd user service, AUR submission
- Learning model improvements — acceptance-history ranking, per-field context analysis
- Screen reader context (exploratory) — stronger context from accessibility APIs for better suggestions
The full session-by-session plan — file layouts, interfaces, and per-session gotchas — lives in PLAN.md.
XType would not exist without Cotypist — Daniel Gräfe's elegant macOS app that demonstrated, with great taste and engineering polish, that on-device AI autocomplete can feel like a native part of an operating system rather than a chat sidebar bolted onto your editor.
Daniel is also the developer behind the long-running Timing productivity app and runs Accelerated Thought GmbH. Cotypist's UX, its commitment to running entirely on-device, and its accuracy to the users writing style inspired this project exists in the first place.
If you are on macOS, please support his work directly: https://cotypist.app.
XType is an independent re-implementation for Linux. It is not affiliated with, endorsed by, or derived from Cotypist's source code — only inspired by the experience it pioneered.
Additional thanks to:
- David Edmundson (KDE) — whose input-method-playground was the canonical reference for getting an input method talking to KWin on Wayland.
- The Fcitx5 and IBus maintainers — for two excellent, well-documented input method frameworks.
- The Ollama team — for making local LLM serving a one-line install.
- The Qwen 2.5 team at Alibaba — for releasing a small model that is genuinely good at sentence continuation.
- The Vocalinux project — prior art validating custom IBus engines on KDE Wayland.
MIT — see LICENSE for the full text.
XType is built and maintained by Kevin Sugar on CachyOS / KDE Plasma. Issues, ideas, and pull requests are welcome.