Chinese on a GUI-less Ubuntu server: fbterm for display, fcitx5+rime for input
Your server has no GUI — just the bare console TTY — and you want to read and type Chinese on it. Installing fonts does nothing: the kernel console draws with a ~256-glyph VGA font baked into the driver, so CJK renders as diamonds no matter what you install. The fix has two halves: fbterm draws text onto the framebuffer itself (display), and fcitx5 with its fbterm frontend supplies the input method (typing). Here's the whole setup, including the three gotchas that cost me an evening.
Everything below was done on Ubuntu 24.04 (Mint 22.3) over SSH; the target machine is an old laptop used headless. Swap davidwei for your user.
Display: fbterm
sudo apt install fbterm fonts-wqy-microhei
sudo usermod -aG video $USER # needed for /dev/fb0; re-login to apply
Log in on the physical console and run fbterm — it opens a full terminal on the framebuffer using fontconfig fonts. It cannot start over SSH (no framebuffer there, and you don't need it: your SSH client renders CJK itself).
Font gotcha #1: fbterm renders TrueType fonts well but tends to clip outline fonts at the right/bottom of the character cell, and --font-width/--font-height only change the cell metrics — they don't scale the glyphs. WenQuanYi Zen Hei Mono with an 8px cell worked for me where Noto CJK and default settings clipped. Put it in ~/.fbtermrc:
font-names=WenQuanYi Zen Hei Mono
font-size=16
font-width=8
Font gotcha #2, the sneaky one: fbterm rewrites ~/.fbtermrc back to defaults when it exits. Your font (and later your input-method line) silently vanishes. Make it read-only at the filesystem level:
chattr +i ~/.fbtermrc
fbterm can no longer "helpfully" save its config. Edit it with sudo when needed, and re-apply +i after.
Input: fcitx5 + the fbterm frontend
Ubuntu 24.04 ships a packaged fbterm frontend for fcitx5, which most old guides miss (they point at the dead ibus-fbterm / ucimf):
sudo apt install fcitx5 fcitx5-rime fcitx5-frontend-fbterm
I use the rime engine with the 五笔拼音 schema (wubi codes and pinyin both work); plain pinyin via fcitx5-chinese-addons exists too, but its engine produced raw passthrough for me on this setup. Pick your schema in rime's user config:
mkdir -p ~/.local/share/fcitx5/rime
cat > ~/.local/share/fcitx5/rime/default.custom.yaml <<'EOF'
patch:
schema_list:
- schema: wubi_pinyin
- schema: luna_pinyin
EOF
Tell fbterm which IM program to spawn — this line must be in ~/.fbtermrc (see the chattr +i above):
input-method=fcitx5-fbterm
Now the gotcha that ate most of my evening. Kill fcitx5 before you edit ~/.config/fcitx5/profile — like fbterm, fcitx5 rewrites the profile with its in-memory state on exit, silently reverting your edit. The profile defines the input method group, and this is where the real bug lives. A default group looks like:
[Groups/0/Items/0]
Name=keyboard-us
[Groups/0/Items/1]
Name=rime
Sounds harmless, but fcitx5 tracks one active IM per group, and it stays on keyboard-us forever — Ctrl+Space only toggles the current IM on/off, it does not switch between group items. So you press Ctrl+Space, see nothing, type hanzi, and it lands as raw ASCII every time. With a GUI you'd never notice (the panel shows the active IM); headless you just think everything is broken.
Fix: make the group contain rime only — rime has its own Shift-for-English mode, so you lose nothing:
pkill -x fcitx5 # FIRST — order matters
cat > ~/.config/fcitx5/profile <<'EOF'
[Groups/0]
Name=Default
Default Layout=us
DefaultIM=rime
[Groups/0/Items/0]
Name=rime
Layout=
[GroupOrder]
0=Default
EOF
Then start it and verify:
export DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u)/bus
fcitx5 -d
fcitx5-remote -n # MUST print: rime
If this prints anything else, the engine will never see a keystroke. On first start rime deploys its tables (slow on old hardware — the build lands in ~/.local/share/fcitx5/rime/build).
Auto-start on console login
Add to ~/.bashrc so a console login drops you straight into a Chinese-capable fbterm, without triggering over SSH or recursing inside fbterm (fbterm keeps TERM=linux in its child shell, so the guard uses its own marker):
# auto-start fbterm on bare console TTYs
if [ "$TERM" = "linux" ] && [ -z "$FBTERM_AUTO" ] && [ -t 0 ]; then
[ -n "$DBUS_SESSION_BUS_ADDRESS" ] || export DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$(id -u)/bus"
pgrep -u $USER -x fcitx5 >/dev/null || fcitx5 -d >/dev/null 2>&1
FBTERM_AUTO=1 fbterm -i fcitx5-fbterm
fi
If you also get fbterm's can't change kernel keymap table warning (all shortcuts dead), grant it just the capability it needs instead of setuid root:
sudo setcap cap_sys_tty_config+ep /usr/bin/fbterm
Note an apt upgrade of fbterm resets that.
How to use it
At the physical console, log in — fbterm starts automatically. Ctrl+Space activates rime, type wubi codes or pinyin, space commits the first candidate, Shift flips to temporary English. Over SSH none of this is needed: input happens client-side through your local machine's IME, and the server only needs a UTF-8 locale.
One debugging trick that cracked the case for me, since a headless IM has so many silent layers: capture what actually gets committed. Run cat > /tmp/test, type through the IM, Ctrl+D, then xxd /tmp/test. Correct UTF-8 bytes (e6 b1 89 for 汉) means the pipeline works; raw ASCII means the keys never reached the engine; mojibake means an encoding leg is broken. Three different failures, three different fixes.