28 янв. 2008 г.

Emacs, X Window и единое переключение раскладки клавиатуры

Aim

Цель: Сделать переключение раскладки X Window и внутренней раскладки Emacs по одним и тем же комбинациям клавиш: Fn+j (английский), Fn+k (русский), Fn+l (итальянский).


X Window

Добавляем Option "XkbLayout" "us,ru(winkeys),it" в секцию "Input Device" в файл xorg.conf


emxkb

Смотрим http://akshaal.livejournal.com/58473.html, модифицируем код, получаем emxkb.c . Компилируем:

  
    gcc -L/usr/X11R6/lib -lX11 -o emxkb emxkb.c
  
При исполнении emxkb 0 раскладка переключается на "us", emxkb 1 -- "ru(winkeys)", emxkb 2 -- "it". А если значение WM_CLASS окна равно строке "emacs", то emxkb шлет нажатие клавиш F31, F32 или F33 (в зависимости от параметра) и устанавливает раскладку для X Window на "us".


emxkb.c:

  
/* Thanks to Evgeny Chukreev (C) 2005, GNU GPL */
/* See http://akshaal.livejournal.com/58473.html */

/* Modified a little by Bzek .) */

/* Compilation: gcc -L/usr/X11R6/lib -lX11 -o emxkb emxkb.c */

/* Usage:
 *      emxkb 0  # For first group
 *      emxkb 1  # For second group 
 *      emxkb 2  # For third group
 */

#include <X11/Xlib.h>
#include <X11/XKBlib.h>
#include <X11/keysym.h>
#include <X11/Xutil.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* For locking a group */
int lock_group (int window_id, int group);

/* For sending a key to Emacs */
int send_key_to_emacs (Display *display, int window_id, int group);


/* Main function */
int
main (int argc, char *argv[]) {

    Display *display;
    XClassHint class_hint;
    Window focus;
    int revert, group;

    /* Check arguments */
    if (argc < 2) {
        fprintf (stderr, "Usage: %s <group>\n", argv[0]);
        return -1;
    }

    /* Parse arguments */
    group = strtol (argv[1], NULL, 0);

    /* Open display */
    display = XOpenDisplay (NULL);

    if (!display) {
        fprintf (stderr, "%s: Can't open display\n", argv[0]);
        return -2;
    }

    /* Get window id */
    if (!XGetInputFocus (display, &focus, &revert)) {
        fprintf (stderr, "%s: Can't get focus\n", argv[0]);
        return -3;
    }

    /* Get WM_CLASS */
    XGetClassHint (display, focus, &class_hint);
/*     printf ("WM_CLASS: %s\n", class_hint.res_name); */

    if (class_hint.res_name != NULL
        && !strncmp (class_hint.res_name, "emacs", strlen("emacs"))) {
        /* Yeah, Emacs! Send a key. */
        if (!send_key_to_emacs (display, focus, group)) {
            fprintf (stderr, "%s: Failed during sending a key to Emacs\n", argv[0]);
            return -4;
        }

        /* Change group to 0 (it should be "us") for Emacs */
        group = 0;
    }

    if (!lock_group (focus, group)) {
        fprintf (stderr, "%s: Failed during locking group\n", argv[0]);
        return -5;
    }

    /* Close display */
    XCloseDisplay (display);

    /* That is all */
    return 0;
}


int
send_key_to_emacs (Display *display, int window_id, int group) {

    XKeyEvent event;

    /* Init event */
    memset (&event, 0, sizeof (event));
    event.window = window_id;
    event.display = display;
    event.root = RootWindow (display, DefaultScreen (display));
    event.state = 0;

    switch (group) {
    case 0:
        event.keycode
            = XKeysymToKeycode (display, XStringToKeysym ("F31"));
        break;
    case 1:
        event.keycode
            = XKeysymToKeycode (display, XStringToKeysym ("F32"));
        break;
    case 2:
        event.keycode
            = XKeysymToKeycode (display, XStringToKeysym ("F33"));
        break;
    default:
        event.keycode
            = XKeysymToKeycode (display, XStringToKeysym ("F31"));
        break;
    }

    /* Send KeyPress event */
    event.type = KeyPress;

    if (!XSendEvent (display, window_id, False, 0, (XEvent *) &event)) {
        fprintf (stderr, "send_key_to_emacs(): Can't send KeyPress event\n");
        return -1;
    }

    /* Send KeyRelease event */
    event.type = KeyRelease;

    if (!XSendEvent (display, window_id, False, 0, (XEvent *) &event)) {
        fprintf (stderr, "send_key_to_emacs(): Can't send KeyRelease event\n");
        return -2;
    }

    XSync (display, False);

    return 1;
}


int
lock_group (int window_id, int group) {

    Display *xkb_display;
    int res;

    /* Open xkb display */
    xkb_display = XkbOpenDisplay (NULL, NULL, NULL, NULL, NULL, NULL);

    if (!xkb_display) {
        fprintf (stderr, "lock_group(): Can't open display\n");
        return -1;
    }

    /* Init XKB */
    res = XkbQueryExtension (xkb_display, NULL, NULL, NULL, NULL, NULL);
    if (!res) {
        fprintf (stderr, "lock_group(): Can't init XKB\n");
        return -2;
    }

    /* Set Focus */
    if (window_id > 0) {
        XSetInputFocus (xkb_display, window_id, RevertToParent, CurrentTime);
        XSync (xkb_display, False);
    }

    /* Lock Group */
    res = XkbLockGroup (xkb_display, XkbUseCoreKbd, abs (group % 4));
    if (!res) {
        fprintf (stderr, "lock_group(): Can't lock group\n");
        return -3;
    }

    XSync (xkb_display, False);
            
    /* Close xkb display */
    XCloseDisplay (xkb_display);

    return 1;
}
  

Window Manager

Запускаем xev, жмем Fn+j, Fn+k, Fn+l, получаем что-то типа:

  
    state ..., keycode ... (keysym ..., KP_End), ...
    state ..., keycode ... (keysym ..., KP_Down), ...
    state ..., keycode ... (keysym ..., KP_Next), ...
  
То есть, для Fn+j (вероятно в вашем случае будет не KP_End) -- это KP_End, Fn+k -- KP_Down, Fn+l -- KP_Next Теперь прописываем в window manager, чтобы по KP_End запускалось emxkb 0, по KP_Down -- emxkb 1 и по KP_Next -- emxkb 2 .



xmodmap

Определеям keycode для F31, F32, F33:

  
xmodmap -e "keycode 241 F31"
xmodmap -e "keycode 242 F32"
xmodmap -e "keycode 243 F33"
  
На будущее добавляем в файл ~/.xmodmaprc строки:
  
keycode 241 F31
keycode 242 F32
keycode 243 F33
  
В ~/.xinitrc добавляем:
  
    xmodmap ~/.xmodmaprc
    

Emacs

Добавляем в .emacs (или init.el) строки:

  
(defun reset-flyspell-with-new-dict (dict)
  "Set new dictionary and restart flyspell"

  (unless (equal dict ispell-local-dictionary)
    (setq ispell-local-dictionary dict)
    (when flyspell-mode
      (flyspell-mode)
      (flyspell-mode)))

  (when flyspell-mode
    (save-excursion
      (flyspell-region (window-start) (window-end))))

  (message nil))

(global-set-key [(f31)]
                (lambda ()
                  (interactive)
                  ;; (reset-flyspell-with-new-dict "american")
                  (inactivate-input-method)))

(global-set-key [(f32)]
                (lambda ()
                  (interactive)
                  ;; (reset-flyspell-with-new-dict "russian")
                  (set-input-method 'russian-computer)))

(global-set-key [(f33)]
                (lambda ()
                  (interactive)
                  ;; (reset-flyspell-with-new-dict "italian")
                  (set-input-method 'italian-keyboard)))


(defun toggle-specified-isearch-input-method (new-input-method)
  "Toggle specified input method in interactive search."
  (interactive)
  (let ((overriding-terminal-local-map nil)))

  (if (eq new-input-method 'default-method) 
      (inactivate-input-method)
    (set-input-method new-input-method))
    
  (setq isearch-input-method-function input-method-function
        isearch-input-method-local-p t)
  (setq input-method-function nil)
  (isearch-update))


(add-hook 'isearch-mode-hook
          (lambda ()
            (define-key isearch-mode-map (kbd "<f31>") 
              (lambda ()
                (interactive)
                (toggle-specified-isearch-input-method 'default-method)))

            (define-key isearch-mode-map (kbd "<f32>") 
              (lambda () 
                (interactive) 
                (toggle-specified-isearch-input-method 'russian-computer)))

            (define-key isearch-mode-map (kbd "<f33>") 
              (lambda () 
                (interactive) 
                (toggle-specified-isearch-input-method 'italian-keyboard)))))
  
Для необходимости можно раскомментировать строки с вызовом (reset-flyspell-with-new-dict).



xxkb

Если есть желание использовать xxkb, то вот конфиг, с которым был протестирован весь этот "костыль":

  
XXkb.group.base: 1
XXkb.group.alt: 2

XXkb.mainwindow.type: normal
! possible values - normal, top, tray, wmaker

XXkb*label.text.1: En
XXkb*label.text.2: Ru
XXkb*label.text.3: It

XXkb.mainwindow.label.enable: yes
XXkb.mainwindow.enable: yes
XXkb.mainwindow.appicon: no
XXkb.mainwindow.geometry: 15x15-0+0
XXkb.mainwindow.label.background: yellow
XXkb.mainwindow.label.foreground: blue4
XXkb.mainwindow.label.font: -misc-*-r-*-13-*

XXkb.button.enable: no

XXkb.controls.add_when_start: yes
XXkb.controls.add_when_create: yes
XXkb.controls.add_when_change: no
XXkb.controls.two_state: no
XXkb.controls.button_delete: yes
XXkb.controls.button_delete_and_forget: yes
XXkb.controls.mainwindow_delete: yes
XXkb.ignore.reverse: no

XXkb.app_list.wm_class_class.alt_group1: emacs Emacs
  

Последняя строчка впрочем даже и не нужна.

Порой (у меня это случалось только с одним приложением и довольно редко) xxkb начинает капризничать и менять обратно раскладку, тогда помогает клик по значку xxkb (чудесатое решение :)).

В xmonad и некоторых других wm, по свидетельствам очевидцев, xxkb вообще не хочет работать с этим "костылем", не давая переключать раскладку с помощью emxkb. Если для вас это так, тогда стоит посмотреть в сторону другого индикатора раскладки клавитуры (например в сторону xneur).



Тестировалось под X.Org 6.7, xxkb (1.10 и 1.11), icewm, fluxbox, ion3, awesome.

При замечаниях и более элегантных решениях просьба оставить комментарий.

Спасибо Павлу Вязовому за помощь.

2 комментария:

Павел Вязовой комментирует...

Описал вариант решения проблемы с помощью scim, читать здесь.

Павел Вязовой комментирует...

Правильный формат для xmodmap:
keycode 241 = F31