Yet another solution for multilingual environments with Emacs

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. Но тут что-то про­рва­ло, и за ве­чер в об­щих чер­тах этот ваш Хас­кель изу­чил.

В чем суть ре­ше­ния:

  1. Вмес­то внеш­них xbindkeys об­ра­бот­чи­ком кнопок пе­ре­клю­че­ния рас­кла­док бу­дет xmonad;
  2. xmonad по­мнит со­сто­яние те­ку­щей рас­клад­ки;
  3. xmonad при со­бы­тии «сме­ни­лось ок­но» про­ве­ря­ет класс ок­на. Если это Emacs, то с по­мощью xkblayoyt те­ку­щий XIM layout вы­став­ля­ет в en и шлет ок­ну код кноп­ки mod1-F11 или mod1-F12 — в за­ви­си­мос­ти от за­пом­нен­ной рас­клад­ки. Если ок­но не Emacs, то с по­мощью xkblayoyt вы­став­ля­ет со­хра­нен­ную в со­сто­янии рас­клад­ку.
  4. Если по­лу­чи­ли кноп­ку сме­ны рас­клад­ки, то де­ла­ем всё то же са­мое, плюс со­хра­ня­ем но­вое со­сто­яние рас­клад­ки.
  5. В ка­чест­ве бо­ну­са, ме­ня­ем цвет рам­ки ок­на в за­ви­си­мос­ти от рас­клад­ки.

Понят­но, что со­сто­яние од­ним дви­же­ни­ем ру­ки мож­но сде­лать пер­со­наль­ным для ок­на и тог­да у каж­до­го ок­на бу­дет своя рас­клад­ка. Но я так не люблю.

Мож­но до­пи­сы­вать вс­якую эв­рис­ти­ку, на­при­мер для тер­ми­на­ла по умо­лча­нию вклю­чать 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)