Skip to content

Give xterm a font that has Cyrillic glyphs - #5

Merged
PhysShell merged 5 commits into
mainfrom
claude/fix-xterm-cyrillic
Sep 8, 2026
Merged

Give xterm a font that has Cyrillic glyphs#5
PhysShell merged 5 commits into
mainfrom
claude/fix-xterm-cyrillic

Conversation

@PhysShell

Copy link
Copy Markdown
Owner

Typing Russian in the guest terminal over RDP produced boxes; switching the host back to a Latin layout worked. That looks like a keyboard problem and isn't one.

What is actually wrong

The input path is correct end to end. The guest runs services.xserver.xkb.layout = "us" and never sees a Cyrillic keysym of its own — the host owns the layout, and xrdp translates through its own keymaps (km-00000419.ini for Russian ships with the image, confirmed present in /etc/xrdp). The characters arrive intact. xterm just cannot draw them.

xterm's compiled-in default is the bitmap fixed font in ISO-8859-1, which contains no Cyrillic glyphs, so each one renders as a box. The fonts were never missing either — the image carries 75 Cyrillic-capable faces (DejaVu, Liberation) and the locale is en_US.UTF-8:

$ fc-list :lang=ru | wc -l
75
$ locale
LANG=en_US.UTF-8

Why a wrapper and not X resources

X resources would be the idiomatic fix, but nothing in this image loads them: there is no display manager session and no xrdb call anywhere in the session path, and ~/.Xresources would live on the persistent home disk rather than in Nix — exactly the kind of state this appliance is built to avoid.

So the defaults go into the binary the Openbox menu actually invokes. lib.hiPrio makes the wrapper win the name collision against xterm in the system profile; the rest of the package (resize, uxterm) is untouched.

Verification

Both variants launched side by side in the same live xrdp session, same text in each:

Window Font Result
1-DEFAULT-FONT stock xterm boxes instead of Кириллица: АБВГДЕ абвгде
2-DEJAVU-U8 -fa 'DejaVu Sans Mono' -fs 11 -u8 renders correctly

nix flake check passes.

🤖 Generated with Claude Code

Typing Russian over RDP produced boxes instead of characters, while switching
the host back to a Latin layout worked fine. That reads like a keyboard problem
and is not one: the input path is correct end to end. The guest runs
`services.xserver.xkb.layout = "us"` and never sees a Cyrillic keysym of its
own - the host owns the layout, and xrdp translates through its own keymaps
(km-00000419.ini for Russian ships with the image). The characters arrive
intact; xterm simply cannot draw them.

xterm's compiled-in default is the bitmap "fixed" font in ISO-8859-1, which
contains no Cyrillic glyphs at all, so every one of them renders as a box. The
fonts themselves were never missing - the image carries 75 Cyrillic-capable
faces (DejaVu, Liberation) and the locale is en_US.UTF-8.

X resources would be the usual fix, but nothing in this image loads them: there
is no display manager session and no xrdb call anywhere, and ~/.Xresources
would live on the persistent home disk rather than in Nix. So the defaults go
into the binary that the Openbox menu actually invokes, with hiPrio so the
wrapper wins the name collision against xterm in the system profile. The rest
of the package (resize, uxterm) is untouched.

Verified on the live VM by running both variants side by side in the same xrdp
session: the stock xterm shows boxes for "Кириллица: АБВГДЕ абвгде", the
wrapped one renders it correctly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 8, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 79114bf0-457e-4109-86a4-e7ad1c10b3a4


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

PhysShell and others added 4 commits September 8, 2026 10:50
Changing the keyboard layout on the Windows side has no effect on a running
session: xrdp applies the client's layout once, from the Client Info PDU at
connect time, and sends bare scancodes afterwards. Measured in a live session
whose guest config says `layout = "us"`:

    $ setxkbmap -query
    layout:     ru

So the guest is not ignoring the client - it was told exactly once, and a later
switch on the host never reaches it. Reconnecting is currently the only way.

Wrap the session so that, after xrdp has applied the client's layout, a Latin
group is placed next to it together with a switch key. The layout list is
deliberately not hardcoded: whatever this particular client negotiated is what
gets the companion group, so a German client gets "us,de" and a Russian one
"us,ru", while a Latin-only client keeps its single group and sees no change.
Pinning "us,ru" into the image would have tied a redistributable appliance to
one person's keyboard.

Both ends are configurable via qubix.keyboard.latinGroup and .toggle. The
default toggle is grp:win_space_toggle to match the Windows shortcut.

Verified by hand in the live session: setxkbmap with these arguments produces
`layout: us,ru` / `options: grp:win_space_toggle` and switching works without
reconnecting or touching the host layout. The generated wrapper passes `sh -n`
and `nix flake check`, but has not yet run inside a rebuilt image -- that comes
with the next image build.

Two limits are documented in the README rather than papered over: Win keys only
reach the guest when mstsc is full screen, and reconnecting to an existing
session does not re-run the wrapper.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The previous commit forwarded services.xserver.xkb.options into the session's
setxkbmap call. Its NixOS default is terminate:ctrl_alt_bksp, which xrdp never
applies by itself -- so passing it through would newly arm Ctrl+Alt+Backspace
to kill the X server. In a kiosk session that is not a debugging convenience:
the RDP window simply disappears on a stray keypress, mid-track.

Caught on the live VM: pressing the combination closed the session while the
option was set by hand, and stopped doing anything after reconnecting, when
xrdp had reset the keymap without it. Only the group toggle is set now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The previous attempt did not work on a rebuilt image, and hid why: it called
setxkbmap once from startwm.sh and ended the line with `|| true`, so the
failure went nowhere. On the live VM the result was visible as a missing
options line entirely:

    $ setxkbmap -query
    rules:      base
    model:      pc104
    layout:     us

No grp:win_space_toggle at all, although running the same setxkbmap by hand in
that very session worked. X is simply not guaranteed to accept connections yet
when startwm.sh runs, and xrdp may also apply the client's layout after the
session script has started.

So do it from a background loop that checks the result and re-applies while the
toggle is missing, for a dozen rounds, and report failures on stderr instead of
swallowing them.

The second half of the problem was a design mistake. The dynamic default adds a
Latin group beside whatever xrdp negotiated, which is right for keeping the
image client-agnostic -- but RDP reports only the client's *active* layout, so
connecting from a US-layout Windows meant a single group and nothing to toggle
to, no matter how well the code ran. Add qubix.keyboard.layouts for naming the
list outright, keep the dynamic behaviour as the default, and set "us,ru" on
spotibox, which is a specific machine with a fixed address and a lab password
rather than a redistributable template.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every `up` or `recreate` from a .gz leaves an unpacked copy behind, and release
downloads keep a directory per tag, so the cache grows without anything ever
clearing it. On the machine this was written on it had reached 5.89 GB of images
that no longer corresponded to anything installed.

    qubixctl -Command gc              # dry run: what would go, and how much
    qubixctl -Command gc -Force       # delete it
    qubixctl -Command gc -Force -All  # the installed image's cache too

Nothing is deleted without -Force, and the dry run reads only, so it needs no
elevation. The directory matching image-version.txt is kept by default, because
re-fetching a release means downloading the assets again; `local/` is always
dropped, since it only holds a copy unpacked from a file the caller still has.
Non-release markers ('file:...', 'wsl:...') cannot name a cache directory, so
they protect nothing -- which the unit checks pin down.

The VM directory is never a candidate: not the home disk, which is the only
state in the system, and not the live system disk.

Images staged by hand under vmRoot are reported and never deleted. Cleaning up
after the controller is one thing; deleting files a person placed themselves is
another, and the useful half is simply saying that 18 GB is sitting there.

The decision half is a pure function taking directory names, so it is unit
checked without a disk; sizing and the foreign-image scan are checked against a
temporary tree. 55 checks pass under both Windows PowerShell 5.1 and pwsh 7,
PSScriptAnalyzer is clean, and the dry run was exercised against the real cache.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@PhysShell
PhysShell merged commit ef71bdb into main Sep 8, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant