TLDR: I can now use Emacs with OS-level Ukrainian keyboard layout (_) and still have all Emacs functional keys like C-f work as desired. Thus, I can abandon set-input-method for good and never switch layouts _inside Emacs anymore.


Emacs has its own keyboard layout definition and switching machinery independent of OS / windowing system / terminal.

The idea is that this allows Emacs to use your desired keyboard layout for text input, but still use English for everything else (commands like C-f or C-x b, minibuffer input, etc). Thus the “standard way” to use a non-English keyboard layout in Emacs is to switch layouts inside Emacs via set-input-method and toggle-input-method, while the layout outside Emacs remains the same. The layouts in Emacs are defined in terms of translating the characters of a QWERTY English keyboard layout to other layouts (this has its own set of problems if your primary OS layout isn’t QWERTY, but that’s a different issue that fix-input addresses).

The problem is - if you also need to change keyboard layouts outside Emacs (as most people would probably do), this leaves you with two conflicting layout switching mechanisms: if your current OS layout is Ukrainian and you are trying to do C-a in Emacs it would fail (because Emacs would see C-ф instead). So in order to switch from typing in Ukrainian in, say, Firefox, to typing in Ukrainian in Emacs, you would have to switch OS layout to English for Emacs, and then switch to Ukrainian inside Emacs.

Прив’язати українські клавіші до латинських таким чином, щоб функційні клавіші Emacs працювали в обох розкладках

  (defun translate-key-with-modifiers (from to)
    (let ((from* (char-to-string from))
          (to* (char-to-string to)))
      (define-key function-key-map (kbd from*) (kbd to*))
      (define-key function-key-map (kbd (concat "C-" from*)) (kbd (concat "C-" to*)))
      (define-key function-key-map (kbd (concat "M-" from*)) (kbd (concat "M-" to*)))
      (define-key function-key-map (kbd (concat "C-M-" from*)) (kbd (concat "C-M-" to*)))))

  (seq-mapn #'translate-key-with-modifiers 
            (concat "'1234567890-="
                    "ʼ!\"№;%:?*()_+" ; with shift
            (concat "$&[{}(=*)+]!#"

                    "~%7531902468`"  ; with shift