Skip to content

biglinux/bigcedilla

Repository files navigation

BigCedilla

A tiny evdev/uinput daemon that restores the Brazilian ç when the dead-acute key (' / ´) is followed by c — rewriting the key below the compositor, so the fix reaches Chromium and Electron too.


Why this exists

Since GTK 4.20 (GNOME/gtk MR 8556, "wayland: Stop deriving from GtkIMContextSimple", merged 2025-05-13) the GTK4 Wayland backend no longer does client-side compose — it delegates dead keys and compose sequences to the compositor. Chromium and Electron never used the Wayland input method for this in the first place. The result on Brazilian and US-International layouts:

'  +  c   →   ć   (U+0107, c-with-acute)   ← wrong

expected:

'  +  c   →   ç   (U+00E7)
'  +  C   →   Ç

GTK_IM_MODULE=simple papers over it for GTK apps, but not for Chromium or Electron. A compositor input method (fcitx5, or the earlier input-method-v2 build of bigcedilla) can't help either: Chromium ignores the Wayland IM, and KWin only routes keys to the IM when a client activates text-input, which Chromium does not.

Nor can a ~/.XCompose rule: Chromium reads the compose table for the process locale and ignores that file entirely (measured — see Verification evidence).

The only layer that reaches every client — Chromium, Electron, GTK, Qt, X11 — is below the toolkit, at the kernel input level. That is where bigcedilla works.


How it works

      kxkbrc change                one-shot                 state file
   ────────────────► systemd path ─────────► publisher ───► /run/user/<uid>/bigcedilla/keymap
                                             (exits)                    │ inotify
                                                                        ▼
 physical keyboard ─grab(EVIOCGRAB)─► daemon (root) ─uinput─► compositor ─► every app
                                       │
                                       └─ dead_acute + c  →  ç keycode
                                          everything else →  forwarded verbatim

One resident process. The root daemon grabs every physical keyboard with EVIOCGRAB and creates one uinput virtual keyboard. It runs keys through a small state machine: dead_acute then c emits the active layout's ç keycode (not a Unicode string) — the compositor turns that keycode into ç for whatever app is focused, Chromium included. Held Shift rides along and yields Ç. Everything else is forwarded untouched, so normal compose (á, é, ´ ), the crase Shift+'`, shortcuts, and other layouts keep working.

The daemon never polls. It sleeps in poll() on inotify and wakes only when a keyboard is hotplugged, the login session changes, the system keyboard configuration is edited, or a new layout is published — so an idle machine pays essentially nothing for it.

No resident user process. KDE stores the keyboard choice in kxkbrc, inside $HOME, which the root daemon has no business reading. A systemd path unit watches that file and starts a one-shot publisher, which writes a closed profile (br, us-intl, or off) to a private state file and exits. The daemon accepts files only from the active local seat and maps those profiles to built-in, xkb-tested keycodes; user-controlled raw keycodes never cross into root. The publisher has no device access, so no app gains raw input.

The publisher writes with a unique, private temporary file, fsync, atomic rename, and a directory fsync. Both sides reject relative or symlinked runtime paths, non-private ownership/modes, non-regular state files, extra hard links, oversized content, and every profile outside br, us-intl, or off.

Because the daemon sits at evdev, it coexists with maliit and fcitx5 — those live at the compositor input-method layer and bigcedilla never touches that slot.

Layout awareness

The ç fix needs different keycodes per layout, so the layout is resolved with xkbcommon rather than hard-coded:

  • ABNT2 — acute on KEY_LEFTBRACE, ç on KEY_SEMICOLON (base plane).
  • us-intl — acute on KEY_APOSTROPHE, ç on AltGr + comma (injected).

Layouts with no dead_acute (French AZERTY, plain US, us alt-intl) resolve to nothing and the daemon passes those keys through untouched — it never grabs the wrong key.

Sources are tried in order, and a change to any of them is applied live:

  1. /etc/X11/xorg.conf.d/00-keyboard.conf. Despite the path this is not X11-only configuration: it is where systemd-localed stores the system keyboard layout, and Wayland compositors read it too. Written by localectl set-x11-keymap.
  2. /etc/vconsole.conf — its XKBLAYOUT, or failing that its console KEYMAP translated through systemd's own kbd-model-map. A machine configured only with localectl set-keymap has just the latter.
  3. XKB_DEFAULT_LAYOUT / _MODEL / _VARIANT / _OPTIONS.
  4. A br default.

A per-session KDE layout, when configured, overrides all of it through the state file.


Install

Arch / BigLinux / Manjaro

cd pkgbuild
makepkg -si

For an explicit local-checkout evidence build, keep the PKGBUILD as the owner and opt in to the validated source root:

cd pkgbuild
BIGCEDILLA_LOCAL_ROOT="$PWD/.." makepkg --cleanbuild

The package enables the root daemon (system preset) and the user-side path unit (global preset). Verify:

systemctl status bigcedilla.service              # root daemon
systemctl --user status bigcedilla-agent.path    # layout watch

Two version numbers, on purpose: following the BigLinux convention the package is versioned by build date and time (pacman -Q bigcedilla26.07.25-0048), while the program keeps a semantic version (bigcedilla --version1.0.0).

Manual

cargo build --release
sudo install -Dm755 target/release/bigcedilla /usr/bin/bigcedilla
sudo install -Dm644 pkgbuild/bigcedilla.service \
    /usr/lib/systemd/system/bigcedilla.service
sudo install -Dm644 pkgbuild/bigcedilla-agent.path \
    /usr/lib/systemd/user/bigcedilla-agent.path
sudo install -Dm644 pkgbuild/bigcedilla-agent.service \
    /usr/lib/systemd/user/bigcedilla-agent.service
sudo systemctl enable --now bigcedilla.service
systemctl --user enable --now bigcedilla-agent.path

Enable the path unit, not the service: the service is a one-shot, so enabling it alone would publish the layout once at login and never notice a later change.


Configuration

Environment variable Effect
XKB_DEFAULT_LAYOUT / _MODEL / _VARIANT / _OPTIONS Override the detected layout when the /etc files are absent.
RUST_LOG Log level, e.g. RUST_LOG=bigcedilla=info. There is deliberately no verbose level — see below.

The command line is intentionally closed: no arguments starts the device daemon, agent runs the one-shot publisher, and resolve [layout [variant]] prints only the built-in profile resolution. --help and --version are non-privileged inspection paths; unknown commands and excess arguments fail instead of falling through to the privileged daemon.

The daemon logs the resolved keycodes at startup, e.g. resolved layout keycodes: trigger=26 ccedilla=39 altgr=false.

It never logs keystrokes. A process that grabs every keyboard must not have a verbose mode that could be switched on in the field, so log::debug! and log::trace! are rejected at compile time (clippy.toml), and the security gate additionally forbids any output statement inside the scopes that see individual key events.


Known limitations

  • A KDE tray layout toggle is not followed. The path unit sees changes to the configured layouts in kxkbrc; which of several configured layouts is active right now lives only in the running session and never touches that file. Changing the configured layout — in System Settings, localectl, or /etc — is applied live.
  • The per-session override is KDE-specific (kxkbrc). On other desktops the daemon uses the system-wide layout, which it does follow live.

Requirements

  • A Linux seat with /dev/uinput and readable /dev/input/event* (the service runs as root with a bounding set containing only CAP_DAC_OVERRIDE, required to traverse the active user's private runtime directory).
  • libxkbcommon and systemd.

Works on any Wayland compositor or Xorg — it is compositor-agnostic.


Verification evidence

Exercised on 2026-07-24/25 in a Manjaro KDE Plasma Wayland VM (kernel 6.18.38, 1280×720), with keystrokes injected as PS/2 scancodes through the hypervisor — below evdev, the same path as a physical keyboard — and results read back as Unicode code points rather than by eye.

  • cargo fmt, Clippy with warnings denied, 36 tests, cargo-deny (advisories, licenses, bans, sources), cargo-audit, cargo-machete, Gitleaks and ShellCheck pass. memmap2 is locked to 0.9.11, the first version patched for RUSTSEC-2026-0186.
  • The daemon resolved the layout from the system configuration, grabbed the emulated AT keyboard, and produced U+00E7 for ´+c and U+00E1 for ´+a in Brave 150.1.92.139 — the cedilla is repaired and normal acute-vowel composition is intact.
  • Idle cost, measured from /proc/<pid>/stat over 60 s with the machine at rest: 0.017 % of one core, 3.3 MB RSS, one resident process. The 0.x polling design measured 4.8 % on the same VM.
  • Hotplug: a uinput keyboard created inside the guest was grabbed sub-second, its removal reaped in ~300 ms, and it was re-grabbed on replug.
  • Live reconfiguration: localectl set-x11-keymap, and vconsole.conf with only a console KEYMAP, both re-point the daemon without a restart — br-abnt2 → 26/39, us-acentos → 40/51 with AltGr, fr → inactive.
  • The user side leaves no resident process: changing kxkbrc triggered the path unit, the one-shot published and exited (Result=success), and the daemon picked the new profile up through inotify.
  • systemd-analyze security bigcedilla.service reports exposure 1.7 OK.
  • Packaging: makepkg from the local checkout produced bigcedilla-26.07.25-0048-x86_64.pkg.tar.zst carrying exactly the seven expected files. Installed with pacman on the VM, pacman -Qkk reports 19 files and 0 altered, the daemon came up enabled and active, the path unit was globally enabled and armed in the running session by the install hook, and the packaged binary produced ç á in Brave. A subsequent package upgrade restarted the daemon, kept the path unit enabled, and left no resident user-side process.

One honest caveat: on that VM the original defect did not reproduce. Brave 150 composes ´+c to ç on its own under a pt_BR.UTF-8 locale, whose compose table carries the rule. The tests above therefore demonstrate correctness and the absence of regressions, not the fix's necessity; the necessity is established by field reports on affected systems. A ~/.XCompose probe confirmed Chromium ignores that file, which rules out the file-only alternative.


License

GPL-3.0-or-later.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages