понедельник, 21 ноября 2005 г.

Топчемся по интернациональным граблям

Эта печальная и утомительная история началась с того, что я решил чуть-чуть поднастроить emacs, а именно мне хотелось иметь возможность выполнять команды привязанные к клавиатурным комбинациям без переключения на латиницу. Да и не плохо бы, чтобы каждый буфер emacs'a помнил свою раскладку, а раскладок то чтобы три штуки было (латиница, русская, украинская) и переключаться между ними не циклически (чтоб не задумываться о текущей раскладке и количестве нажатий для переключения в нужную). Естественно мне не хотелось у emacs'a иметь отдельный от всех остальных программ набор клавиатурных сочетаний для переключения языков. Одни и те же клавиатурные сочетания должны переключать языки как в emacs так и в остальных программах. По ходу мне захотелось, чтобы помимо emacs'a каждое иксовое окно находилось в своей раскладке и каким-то образом ее отображало. Неплохо бы еще, чтобы при переключении раскладок менялся словарик орфографический на соответствующий выбранному языку.

Итогом моего двухдневного труда (помимо нервно изгрызанных ногтей) стало достижение всех выше указанных прихотей и теперь я свободно могу написать:
Hello native England!
Привет родная Россия!
Привіт рідна Україна!

Итак, что мы хотим:
RAlt-1 латиница
RAlt-2 русский
RAlt-3 украинский
(RAlt - правый Alt)

Для начала изучаем документацию по xkb (спасибо Ivan Pascal). Вдумчиво прочитав все от корки до корки пытаемся, сделать первые шаги в написании необходимой конфигурации и сразу выясняем, что setxkbmap ну никак не хочет грузить наши файлы лежащие в домашнем каталоге, ему обязательно их хочется видеть в /usr/-что-то-там-такое. Править системные файлы - очень плохое решение. Если копать дальше, то рано или поздно можно заметить вот такую строку из мана по setxkbmap:
setxkbmap -print us | xkbcomp - $DISPLAY

Аха! Вот оно! Оказывается, с помощью xkbcomp можно не только компилировать конфигурации для xkb, но и сразу их загружать в xkb. Делаем:
setxkbmap > ~/.local-xkb
и берем полученный файл за основу нашей конфигурации.

В конце-концов (потратив кучу нервов, наступив на множество граблей и попробовав несколько неработающих вариантов) для переключения раскладок с помощью задуманных клавиш создаем вот такой файл конфигурации:

// Evgeny "Akshaal" Chukreev (C) 2005

xkb_keymap {
xkb_keycodes {
include "xfree86+aliases(qwerty)"
};

xkb_types {
include "complete"
};

xkb_compat {
include "complete"
};

xkb_symbols {
include "pc/pc(pc104)+pc/us+pc/ru(winkeys):2+pc/ua(winkeys):3"

key <AE01> {
overlay1 = <I71>
};

key <AE02> {
overlay1 = <I72>
};

key <AE03> {
overlay1 = <I73>
};

replace key <I71> {
radiogroup=2, [F31],
actions[Group1] = [ LockGroup(group=1) ]
};

replace key <I72> {
radiogroup=2, [F32],
actions[Group1] = [ LockGroup(group=2) ]
};

replace key <I73> {
radiogroup=2, [F33],
actions[Group1] = [ LockGroup(group=3) ]
};

replace key <RALT> {
[NoSymbol],
actions[Group1] = [ SetControls(controls=overlay1) ]
};
};

xkb_geometry {
include "pc(pc104)"
};
};

Как будет работать икскабэ по этому сценарию: При нажатии на RALT выполняется действие SetControls(controls=overlay1), включающее оверлейный режим. Теперь палец (не отпуская RALT) тянется к кнопкам 1, 2 или даже 3, но допустим к кнопке 2. При нажатии на кнопку два, клавиатура отсылает в икскабэ сканкод клавиши выглядящий в символьном обозначении как <AE02>. Икскабэ смотрит свои таблицы и находит, что для этого сканкода, при включенном оверлейном режиме, надо сделать вид, что клавиатура прислала сканкод с символьным обозначением <I72> (такое поведение определенно строкой overlay1 = <I72>, а сканкод клавиши в оверлейном режиме выбран таким образом, чтлбы клавиатура никогда сама такой сканкод не присала). Теперь икскабэ смотрит запись для сканкода <I72> и выполняет действие actions[Group1] = [ LockGroup(group=2) ], устанавливающее активной вторую группу (согласно этой строке 'include "pc/pc(pc104)+pc/us+p /ru(winkeys):2+pc/ua(winkeys):3"', вторая группа у нас pc/ru(winkeys)). Помимо прочего, икскабэ посылает приложению
событие о нажатии на кнопку со сканкодом I72 и символьным обозначением F32. Теперь мы отпускаем RAlt-2 и икскабэ сбрасывает оверлейный режим. Если кратко то все именно так и работает. Для сочетаний RAlt-1 и RAlt-3 все происходит аналогично, но RAlt-1 устанавливает группу 1 и посылает приложениям сообщение о нажатии F31, а сочетание RAlt-3 соответственно группу 3 и кнопку F33.

Для использования этой конфигурации необходимо выполнить команду:
xkbcomp ~/.local-xkb $DISPLAY
(лучше всего прописать ее в ~/.xsession)

На этом можно было бы и остановиться, но ведь еще нужно настроить емакс, а это самое интересное! Если настройка емакса не нужна, то в конфигурации xkb надо заменить символы F31, F32 и F33 на NoSymbol. Тогда переключение клавиатуры будет незаметным для приложений. Если замену не произвести и больше ничего не делать, то как минимум будут проблемы в шеле, который при нажатии получит загадочную ескэйп последовательность и вставит ее как есть в строку ввода. Но это я продолжил настройку емакса.

Первым делом необходимо придумать как уберечь программы от событий нажатия на несуществующие клавиши F31, F32, F33 в момент переключения раскладок. Прослойкой между иксами и сервером является оконный менеджер, он первым отлавливает нажатия и если они не его, то отдает их дальше программе находящейся в фокусе. К счастью или несчастью имеющимся оконным менеджером является ion3. В нем мы может просто забиндить что-то на эти клавиши и все, приложение о нажатии ничего не узнает. Но ведь нам надо, чтобы емакс таки узнал о нажатии и произвел необходимые действия по переключению своей внутренней раскладки... Одним из вероятных решений была бы возможность забиндить нажатие таким образом, чтобы в момент срабатывания мы могли определить какое приложение находится в фокусе и при необходимости передать ему событие дальше или иначе проигнорировать нажатие. Ion3 поддерживает написание скриптов на языке lua и используя эту возможность мы можем... мы могли бы фильтровать событие нажатия, если бы не очередное, но: в ионе нет луа фукции для передачи приложению события о нажатии клавиши. Да, с помощью луа мы можем отловить нажатие на клавиши F31, F32, F33, мы можем определит какому приложению эти клавиши предназначаются, но мы не можем при надобности передать нажатие приложению. В поисках решения я натолкнулся на пост (за который спасибо) жж юзера besm6 (которого я кажется видел еще в свое время в Фидо). Артем решил проблему передачи нажатия через вызов
внешней простенькой утилитки. Взяв идею пишем вариацию на тему, для передачи произвольного сочетания произвольному иксовому окну:

/* Evgeny Chukreev (C) 2005, GNU GPL */

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

/* Main function */
int main (int argc, char *argv[])
{
Display *display;
XKeyEvent event;
int window_id;
char *keysym;
int state;

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

/* Parse arguments */
window_id = strtol (argv[1], NULL, 0);
keysym = argv[2];
state = strtol (argv[3], NULL, 0);

/* Open display */
display = XOpenDisplay (NULL);
if (!display) {
fprintf (stderr, "%s: Can't open display\n", argv[0]);
return -4;
}

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

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

if (!XSendEvent (display, window_id, False, 0, (XEvent *) &event)) {
fprintf (stderr, "%s: Can't send KeyPress event\n", argv[0]);
return -2;
}

XSync (display, False);

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

if (!XSendEvent (display, window_id, False, 0, (XEvent *) &event)) {
fprintf (stderr, "%s: Can't send KeyRelease event\n", argv[0]);
return -3;
}

XSync (display, False);

/* Close display */
XCloseDisplay (display);

/* That is all */
return 0;
}

Конечно, мы могли бы сделать из этого библиотеку для луа, там это очень просто, но лично я делать этого не стал, ибо луа меня раздражает еще больше чем перл, поэтому перебьется, хотя сделать библиотеку было бы правильнее.

Имея возможность передать событие нажатия в программу, мы можем написать луа код для обработки F31, F32, F33. Вот что получается (aksendkey):
-- Запускаем интернацианализирующие механизмы по кнопке
-- Соответственно разным приложениям можно посылать
-- разные значения групп и разные символы при переключении раскладки
function i18n_by_key (win, key)
if obj_is (win, "WClientWin") then
-- Реальное окно приложения
pass_by_class_and_set_group (win, key, 0, 'Emacs', 0)
else
-- Фигня какая-то, оставляем в латинице
ioncore.exec ("aklockxkbgroup -1 0")
end
end

-- Если класс окна win равен class, то посылаем ей символ keysym в состоянии
-- state и устанавливаем текущую группу в значение group.
-- Иначе ничего не делаем.
function pass_by_class_and_set_group (win, keysym, state, class, group)
if win:get_ident().class == class then
local windowid = win:xid ()
ioncore.exec ("aksendkey " .. windowid .. " " .. keysym .. " " .. state)
ioncore.exec ("aklockxkbgroup " .. windowid .. " " .. group)
end
end
Здесь используется еще одна утилита, которая устанавливает текущую группу в заданное значение. Нужно это для того, чтобы при работе с emacs'ом не было двойного переключения раскладок (один раз через хкб, второй раз в емаксе). Вот она эта программа aklockxkbgroup:

/* Evgeny Chukreev (C) 2005, GNU GPL */

#include <X11/XKBlib.h>
#include <stdio.h>
#include <stdlib.h>

/* Main function */
int main (int argc, char *argv[])
{
Display *display;
int window_id, group, res;

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

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

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

/* Init XKB */
res = XkbQueryExtension (display, NULL, NULL, NULL, NULL, NULL);
if (!res) {
fprintf (stderr, "%s: Can't init XKB\n", argv[0]);
return -3;
}

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

/* Main fun */
res = XkbLockGroup (display, XkbUseCoreKbd, abs (group % 4));
if (!res) {
fprintf (stderr, "%s: Can't lock group\n", argv[0]);
return -3;
}

XSync (display, False);

/* Close display */
XCloseDisplay (display);

/* That is all */
return 0;
}
Теперь биндим луа функцию на клавиши и почти (хехе) все готово. Это прописывается в разделе WMPlex биндингов:

bdoc ("I18n: LAT mode."),
kpress ("F31", "i18n_by_key (_sub, 'F31')"),

bdoc ("I18n: RUS mode."),
kpress ("F32", "i18n_by_key (_sub, 'F32')"),

bdoc ("I18n: UKR mode."),
kpress ("F33", "i18n_by_key (_sub, 'F33')"),
Таким образом при нажатии, например на RAlt-2, срабатывает наш луа скрипт, он проверяет и если текущее онко содержит емакс, то сбрасывает группу в 0 (латиница) и посылает емаксу событие о нажатии клавиши F32.

Теперь пришло время научить emacs реагировать на приходящие к нему нажатия F31, F32, F33. Тут все почти просто. Загвоздка только в том, что я желаю, чтобы русская и украинская расскладки в емакс соответствовали аналогичным
в остальных программах. В конфигурации xkb прописаны (мои любимые) ru(winkeys) и ua(winkeys) - это такие же раскладки как и в виндовс. Так вот, в емакс 21.4 ничего подобного нет. То что там есть для русского языка еще куда ни шло, а украинская раскладка вообще фонетическая, логику которой
я не понимаю. Поэтому пришлось найти последнюю версию пакета leim и выдернуть оттуда файлик cyrillic.el. Кладем его в ~/elisp/updates/, компилируем через M-x byte-compile-file, добавляем в ~/.emacs строчку:
(load-file "~/elisp/updates/cyrillic.elc")


И вот мы имеем методы ввода с названиями: russian-computer, ukrainian-computer. Для переключения методов ввода пишем в ~/.emacs подобный код:

;; Клавиша перехода в режим латиницы
(define-key global-map [(f31)]
`(lambda ()
(interactive)
(inactivate-input-method)
(reset-flyspell-with-new-dict "american")
))

;; Клавиша для перехода в режим русского языка
(define-key global-map [(f32)]
`(lambda ()
(interactive)
(set-input-method 'russian-computer)
(reset-flyspell-with-new-dict "russian")
))

;; Клавиша для перехода в режим украинского языка
(define-key global-map [(f33)]
`(lambda ()
(interactive)
(set-input-method 'ukrainian-computer)
(reset-flyspell-with-new-dict "ukrainian")
))


;; Перезапуск проверки орфографии с новым словарем
(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)
)

Из код видно, что при переключении происходит орфографическая проверка видимой области буфера. На больших файлах теоретически она может сильно тормозить процесс переключения. В таком случаем можно убрать из кода этот участок:

; Перепроверяем видимую область, если флай моде включен
(when flyspell-mode
(save-excursion
(flyspell-region (window-start) (window-end))))
Все бы работало замечательно без очередного напильника, но это же GNU! Проблема: при переключении на ukrainian словарь емакс ругается, что такого словаря немає. Поэтому чтобы можно было переключится на украинский словарик пишем очередной код (записи о наличии украинского словаря в списке ispell-dictionary-alist по-умолчанию нет по понятным причинам о которых скажу дальше, но мы это исправляем приведенным кодом):

;; Возможные словари
(setq ispell-dictionary-alist
'(("american"
"[A-Za-z]"
"[^A-Za-z]"
"[']"
nil
("-B" "-d" "american")
nil
iso-8859-1)

("russian"
"[АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЬЫЪЭЮЯабвгдеёжзийклмнопрстуфхцчшщьыъэюя]"
"[^АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЬЫЪЭЮЯабвгдеёжзийклмнопрстуфхцчшщьыъэюя]"
"[-]"
nil
("-C" "-d" "russian")
nil
koi8-r)

("ukrainian"
"[абвгдґеєжзиіїйклмнопрстуфхцчшщьюяАБВГДҐЕЄЖЗИІЇЙКЛМНОПРСТУФХЦЧШЩЬЮЯ]"
"[^абвгдґеєжзиіїйклмнопрстуфхцчшщьюяАБВГДҐЕЄЖЗИІЇЙКЛМНОПРСТУФХЦЧШЩЬЮЯ]"
"[-']"
nil
("-C" "-d" "ukrainian")
nil
koi8-u)

(nil
"[A-Za-z]"
"[^A-Za-z]"
"[']"
nil
("-B")
nil
iso-8859-1))
)
Уже лучше, но теперь при попытке переключится возникает сообщение о несуществующей кодировке koi8-u! Вот это уже настоящая проблема (собственного поэтому украинского словаря по-умолчанию и нет в списке доступных). Можно конечно поискать и даже наверное найти способ научить емакс новой кодировке. Наверняка цвс емакс эту кодировку знает и если оттуда выдрать нужный файл, то есть некоторая вероятность, что все заработает,
но мне уже лениво, и я все-таки доберусь до Киева и выделенки и сделаю таки apt-get install emacs-snapshot. А пока украинский словарик <<не працюе>>.

Вобщем-то почти все сделано. Осталось решить две небольших (еще раз хехе) проблемы: при переключении между окнами должна восстанавливаться группа локальная для окна.

Пробуем решить проблему через xxkb. Все замечательно, только эпизодически иконки отображаются белым цветом, когда над одним окном несколько таскбаров иконка пказывается не на том... и вообще, если запускать xxkb через .xsession, то он перестает запоминать группы (если запускать с консоли, то все ок), также он мешает работе нашей aklockxkbgroup (xxkb зачем-то по получению события о смене группы пытается выставить ее еще раз). Вобщем убив кучу времи, на поиск проблемы, мы забиваем на нее. Может вообще забить на запоминание группы для окна? Можно, только при переключении с любого окна в емакс нужно сбрасывать текущую раскладку в латиницу, чтобы в емаксе не было двойной перекодировки так сказать (заметно при нажатии shift-цифра). Можно было бы отловить в емаксе события смены фокуса, но судя по исходникам емакса и его документации так просто он это событие не выдаст, в общем у меня не получилось. К томуже, если даже отловить в емаксе событие получения фокуса, то придет оно в момент нажатия клавиши (такой вот он емакс мерзкий), что есть плохо так как мы должны были сбросить раскладку еще ДО нажатия клавиши. Отловить смену фокуса в ion'e тоже не так просто, если вообще возможно.. Поэтому единственный выход - писать программу, отслеживающую ход фокуса и сбрасывающую фокус в латиницу при достижении фокусом емакса. Но не торопимся. Посмотрим сначало свежее мясо и найдем там xkbind - разработка некоего CHG. Судя по описанию, программа замечательная, судя по исходнику хорошо написанная и вообще прелесть. Но она не работает :). Обидно. Открываем исхоники, изучаем процесс передачи фокуса, материм ион, иксы и вообще все. Делаем такой патч для xkbind:

diff -urN old/xkbind.c new/xkbind.c
--- old/xkbind.c 2005-01-03 21:10:21.000000000 +0200
+++ new/xkbind.c 2005-11-21 12:50:14.000000000 +0200
@@ -233,7 +233,9 @@
}
fprintf(stderr, "window: %d\n\n", ev.core.xfocus.window);
#endif
- if(ev.core.xfocus.detail == NotifyNonlinear &&
+ if((ev.core.xfocus.detail == NotifyNonlinear ||
+ ev.core.xfocus.detail == NotifyAncestor ||
+ ev.core.xfocus.detail == NotifyInferior) &&
(ev.core.xfocus.mode==NotifyNormal ||
ev.core.xfocus.mode==NotifyWhileGrabbed) &&
(group=GetWindowGroup(ev.core.xfocus.window))!=-1)
Теперь все замечательно отслеживается. В заголовке приписывается текущий
активный режим и тд.. все как и положено. Прописываем в ~/.xsession что-нибудь типа:
xkbind -label0 '--L-- ' -label1 '==R== ' -label2 '##U## ' -defgrp 0

Все! Счастлив тот, кто не убьет столько времени на брожение по различным граблям,
прочитав это описание, сколько убил я не найдя подобного. %)

Ну вроде ничего не забыл и описал все, что сделал.

ЗЫ По ходу избавился полностью от остатков гнома (типа gnome-session) мешавших работать.

1 комментарий: