Emacs не умеет работать с кнопками альтернативных раскладок. Точнее, умеет слишком хорошо: для этого надо все горячие кнопки заново объявить в альтернативной раскладке. Я лично к таким подвигам не готов.
Есть несколько решений этой проблемы (1, 2), но все они чудовищны и плохо управляемы:
- Первый костыль определяет класс текущего окна и, если это Emacs, вместо смены раскладки шлет код кнопки F31/F32. В Emacs назначаются хуки для обработки этих кнопок, которые переключают внутренний текущий метод ввода Емакса. Проблема в том, что этим способом нам удалось только обеспечить общий интерфейс переключения раскладок в Emacs и остальной системе. Что делаеть, если мы в другом окне переключили XKB на русскую раскладку, а потом пришли в Emacs? У нас не работают все горячие кнопки… То есть, нет согласованного состояния раскладки.
- Второй костыль предлагает нам отказаться от классического X Input
Method (XIM) в пользу UIM. Отлично, но мы не китайцы. Для справки:
альтернативные методы ввода созданы на 100% для предоставления
способа экранного ввода иероглифов. То есть, когда мы выбрали
“традиционный китайский”, а потом последовательно нажимая
x i s m
получили какой-нибудь樣
(как пример). Больше ни для чего эти странные штуки на самом деле не нужны. Зато проблем конфигурирования, сопряженных с отказом от XIM — достаточно.
Ещё один костыль
Давно сижу под xmonad. Конфиг на нём пишется на Хаскеле. Но даже не смотря на пылящуюся дома много лет книжку, мой опыт сильно дальше “Hello world” не ушел. Конфиг к xmonad писал в основном методом copy-paste. Но тут что-то прорвало, и за вечер в общих чертах этот ваш Хаскель изучил.
В чем суть решения:
- Вместо внешних xbindkeys обработчиком кнопок переключения раскладок будет xmonad;
- xmonad помнит состояние текущей раскладки;
- xmonad при событии «сменилось окно» проверяет класс окна. Если это
Emacs, то с помощью xkblayoyt текущий XIM layout выставляет в
en
и шлет окну код кнопкиmod1-F11
илиmod1-F12
— в зависимости от запомненной раскладки. Если окно не Emacs, то с помощью xkblayoyt выставляет сохраненную в состоянии раскладку. - Если получили кнопку смены раскладки, то делаем всё то же самое, плюс сохраняем новое состояние раскладки.
- В качестве бонуса, меняем цвет рамки окна в зависимости от раскладки.
Понятно, что состояние одним движением руки можно сделать персональным для окна и тогда у каждого окна будет своя раскладка. Но я так не люблю.
Можно дописывать всякую эвристику, например для терминала по умолчанию
включать en
.
Настраиваем xmonad
Описываем новый тип и способы работы с ним:
data KeyboardLayout = KbdUs | KbdRu
stringOfKbdLayout KbdUs = "us"
stringOfKbdLayout KbdRu = "ru"
colorOfKbdLayout KbdUs = "#dd7300"
colorOfKbdLayout KbdRu = "#f00000"
emacsKeyOfKbdLayout KbdUs = xK_F11
emacsKeyOfKbdLayout KbdRu = xK_F12
Функция, меняющая цвет заданного окна на указанный цвет (я её где-то скопирайтил):
setWindowBorder' c w = do
XConf { display = d } <- ask
~(Just pc) <- io $ initColor d c
io $ setWindowBorder d w pc
У нас изменилось состояние? Надо послать update в логгер, чтобы xmobar перерисовал заголовок (ему xmonad шлет виртуальную раскладку). Вот эта функция шлет сигнал:
sendUpdateEvent :: () -> X ()
sendUpdateEvent _ =
ask >>= logHook . config
Функции, которые смотрят на текущее окно и в зависимости от его типа и
переданного KeyboardLayout
, меняют раскладку нужным способом,
сохраняя её в state-монаду:
refreshKeyboardLayoutEmacs layout = do
spawn $ "setxkbmap -layout " ++ stringOfKbdLayout KbdUs
sendKey mod1Mask $ emacsKeyOfKbdLayout layout
refreshKeyboardLayoutOtherWindow layout =
spawn $ "setxkbmap -layout " ++ stringOfKbdLayout layout
setKeyboardLayout currentKeyboardLayoutRef layout = do
io $ writeIORef currentKeyboardLayoutRef layout
withWindowSet $ \w -> case Win.peek w of
Nothing -> io $ return ()
Just w -> do
setWindowBorder' (colorOfKbdLayout layout) w
windowClass <- runQuery className w
case windowClass of
"Emacs" -> refreshKeyboardLayoutEmacs layout
_ -> refreshKeyboardLayoutOtherWindow layout
setNewKeyboardLayout :: IORef KeyboardLayout -> KeyboardLayout -> X ()
setNewKeyboardLayout currentKeyboardLayoutRef layout =
do
setKeyboardLayout currentKeyboardLayoutRef layout
sendUpdateEvent ()
На русский будем переключаться по Ctrl-Menu, на английский — по Menu:
myConfig currentKeyboardLayoutRef = def {
`additionalKeys` [
((noModMask, xK_Menu), setKeyboardLayoutWithFocused currentKeyboardLayoutRef KbdUs >>= sendUpdateEvent)
, ((controlMask, xK_Menu), setKeyboardLayoutWithFocused currentKeyboardLayoutRef KbdRu >>= sendUpdateEvent)
-- ...
Взлетаем:
main = do
do currentKeyboardLayoutRef <- newIORef KbdUs
xmonad (myConfig currentKeyboardLayoutRef) {
-- ...
Вот мы в общих чертах воспроизвели то, что делает emxkb из альтернативных решений, и даже больше: мы ещё и state храним. Теперь хотелось бы этот state научиться использовать.
Пропишем функции, которые умеют обновлять раскладку текущего окна на сохраненную. Это просто! Вот функция, которая получает предыдущее окно, текущее окно, ссылку на состояние раскладки и меняет раскладку:
updateKbdLayout :: IORef KeyboardLayout -> Maybe Window -> Window -> X ()
updateKbdLayout currentKeyboardLayoutRef _ currentWindow =
do currentKeyboardLayout <- io $ readIORef currentKeyboardLayoutRef
setKeyboardLayout currentKeyboardLayoutRef currentKeyboardLayout currentWindow
Теперь надо научиться эту функцию звать при переключении окон. Для этого я написал вспомогательную функцию, которая сравнивает сохраненное окно с текущим и, при изменении, зовет переданный коллбэк и сохраняет новое текущее окно:
type WindowSwitchHook = Maybe Window -> Maybe Window -> X ()
windowSwitchHandler :: IORef (Maybe Window) -> WindowSwitchHook -> X () -> X ()
windowSwitchHandler lastWindowRef hook x = do
withWindowSet $ \w -> do
let currentWindow = Win.peek w
lastWindow <- io $ readIORef lastWindowRef
if lastWindow /= currentWindow then
do
io $ writeIORef lastWindowRef currentWindow
hook lastWindow currentWindow
else
io $ return ()
x
Осталось только навесить эту конструкцию на какой-то конец с событиями об изменениях окон. Не придумал ничего лучше, кроме как задружиться с logHook, который все используют как интерфейс в статус-бары:
main = do
do currentKeyboardLayoutRef <- newIORef KbdUs
lastWindow <- newIORef Nothing
xmonad (myConfig currentKeyboardLayoutRef) {
logHook = windowSwitchHandler lastWindow (updateKbdLayout currentKeyboardLayoutRef) $ dynamicLogWithPP (xmobarPrinter currentKeyboardLayoutRef xmobarHdl)
Дополнительно я обрабатываю currentKeyboardLayout в dynamicLogWithPP и имею заметную сигнализацию русской раскладки в статус-баре.
Всю конструкцию в сборе можно посмотреть в моих дотфайлах. Писатель в Хаскеле я ещё тот (целый вечер опыта!), так что извините за возможные упущения.
Обработка кнопок в Emacs
По чьему-то рецепту при русской раскладке дополнительно подсвечиваю красным курсор. В остальном код простой и вряд ли требует пояснений:
(defun my-update-cursor ()
(set-cursor-color
(if (string= current-input-method "russian-computer") "red" "black")))
(add-hook 'buffer-list-update-hook 'my-update-cursor)
(defun my-update-isearch-input-method ()
(if isearch-mode
(progn
(setq isearch-input-method-function input-method-function
isearch-input-method-local-p t)
(isearch-update))))
(defun my-update-input-method (is-ru)
(if is-ru
(set-input-method 'russian-computer)
(inactivate-input-method))
(my-update-isearch-input-method)
(my-update-cursor))
(defun my-select-input-eng ()
(interactive)
(my-update-input-method nil))
(defun my-select-input-rus ()
(interactive)
(my-update-input-method t))
(global-set-key (kbd "<M-f11>") 'my-select-input-eng)
(global-set-key (kbd "<M-f12>") 'my-select-input-rus)
(define-key isearch-mode-map (kbd "<M-f11>") 'my-select-input-eng)
(define-key isearch-mode-map (kbd "<M-f12>") 'my-select-input-rus)