понедельник, 13 августа 2012 г.

nginx: расширяем модуль myutil

Здесь мы начали разрабатывать новый модуль nginx myutil, в котором, как мы задумывали, будут реализованы разные полезные директивы. На данный момент в модуле представлена единственная директива myutil_var_alias. В этой статье будет показано, как организовать поиск по списку переменных с целью получения первого непустого значения. Пусть соответствующая директива будет называться myutil_first_not_empty. Тогда строка в конфигурации вида
            myutil_first_not_empty $fne_data $arg_a $arg_b default;
указывает, что следует найти первое непустое значение среди списка переменных $arg_a и $arg_b (которые соответствуют аргументам a и b строки запроса) и записать его в новую переменную $fne_data; в случае, если переменные $arg_a и $arg_b отсутствуют, либо не содержат непустых значений, в переменную $fne_data следует записать значение default. Будем предполагать, что литералы типа default являются равноправными членами наряду с переменными типа $arg_a и $arg_b и могут записываться где-то в середине списка аргументов (хотя очевидно, что из смысла директивы myutil_first_not_empty следует, что литерал должен быть только один и он должен находиться в конце списка). Сам список аргументов может быть произвольной длины.

Какие типы данных нам понадобятся? Ну, во-первых, как и в случае директивы myutil_var_alias, это массив типа ngx_array_t, соответствующий множеству переменных $fne_... (не забываем совет, приведенный в конце прошлой статьи и добавляем узнаваемый префикс, в данном случае $fne_, ко всем переменным, объявленным нашей новой директивой). Добавляем его в ngx_http_myutil_loc_conf_t:
typedef struct
{
    ngx_array_t  varalias_data;
    ngx_array_t  first_not_empty_var_data;
}  ngx_http_myutil_loc_conf_t;
В директиве myutil_var_alias в качестве элементов массива varalias_data мы использовали простую структуру
typedef struct
{
    ngx_int_t    self;
    ngx_int_t    index;
}  ngx_http_myutil_var_elem_t;
При этом значение self соответствовало индексу новой переменной, созданной директивой myutil_var_alias (ее первому аргументу), а значение index - исходной переменной, т.е. той, значение которой копировалось в новую переменную (это второй аргумент myutil_var_alias).

На этот раз мы имеем дело не с двумя аргументами, а с аргументом - новой переменной, создаваемой директивой myutil_first_not_empty, и списком неопределенной длины. Очевидно, что одним из элементов новой структуры должен быть массив:
typedef struct
{
    ngx_array_t  data;
    ngx_int_t    index;
}  ngx_http_myutil_fne_elem_t;
Здесь data - указанный массив, а index - индекс новой переменной. Какой тип элементов следует выбрать для массива data? Вспомните, что мы хотели записывать в него как переменные, так и литералы. Переменная легко идентифицируется индексом, а литерал - строкой, при этом будем считать, что литерал тоже имеет индекс, равный ( ngx_uint_t )NGX_ERROR: это позволит понимать, с чем мы имеем дело (переменной или литералом) в хендлере ngx_http_myutil_get_first_not_empty_value(), в котором происходит присваивание нового значения переменной $fne_.... Итак, тип элемента из списка аргументов директивы myutil_first_not_empty имеет вид
typedef struct
{
    ngx_str_t    key;
    ngx_int_t    index;
}  ngx_http_myutil_var_handle_t;
Добавляем директиву myutil_first_not_empty в список ngx_http_myutil_commands[], теперь он выглядит так:
static ngx_command_t  ngx_http_myutil_commands[] =
{
    {
        ngx_string( "myutil_var_alias" ),
        NGX_HTTP_LOC_CONF | NGX_HTTP_LIF_CONF | NGX_CONF_TAKE2,
        ngx_http_myutil_var_alias,
        NGX_HTTP_LOC_CONF_OFFSET,
        0,
        NULL
    },
    {
        ngx_string( "myutil_first_not_empty" ),
        NGX_HTTP_LOC_CONF | NGX_HTTP_LIF_CONF | NGX_CONF_2MORE,
        ngx_http_myutil_first_not_empty,
        NGX_HTTP_LOC_CONF_OFFSET,
        0,
        NULL
    },

    ngx_null_command
};
Также обновляем функции создания и слияния конфигураций уровня location:
static void *  ngx_http_myutil_create_loc_conf( ngx_conf_t *  cf )
{
    ngx_http_myutil_loc_conf_t *  lcf = NULL;

    lcf = ngx_pcalloc( cf->pool, sizeof( ngx_http_myutil_loc_conf_t ) );

    if ( ! lcf )
        return NULL;

    if ( ngx_array_init( &lcf->varalias_data, cf->pool, 1,
                         sizeof( ngx_http_myutil_var_elem_t ) ) != NGX_OK )
        return NULL;

    if ( ngx_array_init( &lcf->first_not_empty_var_data, cf->pool, 1,
                         sizeof( ngx_http_myutil_fne_elem_t ) ) != NGX_OK )
        return NULL;

    return lcf;
}


static char *  ngx_http_myutil_merge_loc_conf( ngx_conf_t *  cf, void *  parent,
                                               void *  child )
{
    ngx_http_myutil_loc_conf_t *  prev = parent;
    ngx_http_myutil_loc_conf_t *  conf = child;

    ngx_uint_t                    i;

    /* all data in next sections will be imported from parent location in
     * addition to already collected data in child location */

    for ( i = 0; i < prev->varalias_data.nelts; ++)
    {
        ngx_http_myutil_var_elem_t *  elem = NULL;

        elem = ngx_array_push( &conf->varalias_data );

        if ( ! elem )
            return NGX_CONF_ERROR;

        *elem = ( ( ngx_http_myutil_var_elem_t * )
                                    prev->varalias_data.elts )[ i ];
    }

    for ( i = 0; i < prev->first_not_empty_var_data.nelts; ++)
    {
        ngx_http_myutil_fne_elem_t *  elem = NULL;

        elem = ngx_array_push( &conf->first_not_empty_var_data );

        if ( ! elem )
            return NGX_CONF_ERROR;

        *elem = ( ( ngx_http_myutil_fne_elem_t * )
                                    prev->first_not_empty_var_data.elts )[ i ];
    }

    return NGX_CONF_OK;
}
Хендлером конфигурации при регистрации директивы myutil_first_not_empty в списке ngx_http_myutil_commands[] была объявлена функция ngx_http_myutil_first_not_empty(), вот ее тело (строки пронумерованы для дальнейших комментариев):
367 static char *  ngx_http_myutil_first_not_empty( ngx_conf_t *  cf,
368                                             ngx_command_t *  cmd, void *  conf )
369 {
370     ngx_str_t *                   value     = cf->args->elts;
371     ngx_http_variable_t *         v         = NULL;
372     ngx_http_myutil_loc_conf_t *  lcf       = conf;
373     ngx_int_t                     v_idx     = NGX_ERROR;
374     ngx_uint_t *                  v_idx_ptr = NULL;
375     ngx_http_myutil_fne_elem_t *  resvar    = NULL;
376 
377     ngx_uint_t                    i;
378 
379     if ( value[ 1 ].data[ 0 ] != '$' )
380     {
381         ngx_conf_log_error( NGX_LOG_EMERG, cf, 0,
382                             "invalid variable name '%V'", &value[ 1 ] );
383         return NGX_CONF_ERROR;
384     }
385 
386     value[ 1 ].len--;
387     value[ 1 ].data++;
388 
389     /* It must never occur due to command arguments number restriction */
390     if ( cf->args->nelts < 3 )
391         return NGX_CONF_ERROR;
392 
393     resvar = ngx_array_push( &lcf->first_not_empty_var_data );
394 
395     if ( ! resvar )
396         return NGX_CONF_ERROR;
397 
398     if ( ngx_array_init( &resvar->data, cf->pool, cf->args->nelts - 2,
399                          sizeof( ngx_http_myutil_var_handle_t ) ) != NGX_OK )
400         return NGX_CONF_ERROR;
401 
402     for ( i = 2; i < cf->args->nelts; ++)
403     {
404         ngx_http_myutil_var_handle_t *  res   = NULL;
405         ngx_int_t                       index = NGX_ERROR;
406 
407         ngx_uint_t  isvar = value[ i ].data[ 0 ] == '$' ? 1 : 0;
408 
409         res = ngx_array_push( &resvar->data );
410 
411         if ( ! res )
412             return NGX_CONF_ERROR;
413 
414         if ( ! isvar )
415         {
416             res->index = NGX_ERROR;
417             res->key   = value[ i ];
418             continue;
419         }
420 
421         value[ i ].len--;
422         value[ i ].data++;
423 
424         index = ngx_http_get_variable_index( cf, &value[ i ] );
425 
426         if ( index == NGX_ERROR )
427             return NGX_CONF_ERROR;
428 
429         res->index = index;
430         res->key   = value[ i ];
431     }
432 
433     v = ngx_http_add_variable( cf, &value[ 1 ], NGX_HTTP_VAR_CHANGEABLE );
434 
435     if ( v == NULL )
436         return NGX_CONF_ERROR;
437 
438     v_idx = ngx_http_get_variable_index( cf, &value[ 1 ] );
439 
440     if ( v_idx == NGX_ERROR )
441         return NGX_CONF_ERROR;
442 
443     v_idx_ptr = ngx_pnalloc( cf->pool, sizeof( ngx_uint_t ) );
444 
445     if ( ! v_idx_ptr )
446         return NGX_CONF_ERROR;
447 
448     resvar->index  = v_idx;
449 
450     *v_idx_ptr     = v_idx;
451     v->data        = ( uintptr_t )v_idx_ptr;
452     v->get_handler = ngx_http_myutil_get_first_not_empty_value;
453 
454     return NGX_CONF_OK;
455 }
В строках 370-377 объявлены переменные, используемые внутри функции. В строках 379-384 проверяется, что первый аргумент директивы (переменная $fne_...) начинается с символа $. В строках 386-387 первый символ $ убирается из названия переменной (мы теперь и так знаем, что это переменная, и это нас устраивает). В строках 390-391 убеждаемся, что в директиву передано более одного аргумента (хотя эта проверка излишняя: флаг NGX_CONF_2MORE, указанный при объявлении директивы, не позволит запустить nginx, если в какой-либо из директив myutil_first_not_empty в конфигурации будет менее двух аргументов). В строках 393-400 мы создаем новый элемент массива first_not_empty_var_data из структуры ngx_http_myutil_loc_conf. На этом этапе мы знаем количество элементов внутри его подмассива data (это количество элементов директивы минус два), поэтому сразу же инициализируем data указанным значением элементов.

В строках 402-431 мы проходим по оставшимся аргументам директивы и заполняем инициализированные элементы массива data index и key значениями index и value[ i ] соответственно (строки 429-430). При этом, если текущий аргумент начинается с символа $ (т.е. является переменной), значение index получаем из возвращаемого значения функции ngx_http_get_variable_index(), а если это литерал - то записываем в качестве index значение ( ngx_uint_t )NGX_ERROR. В обоих случаях значением key будет имя текущего аргумента value[ i ] (хотя для аргумента-переменной это значение использоваться не будет).

В строках 433-441 создаем новую переменную v, соответствующую нашей переменной $fne_... и получаем ее индекс v_idx. Далее выделяем память под элемент типа ngx_uint_t (переменная v_idx_ptr): здесь будет храниться значение v_idx, а сам этот участок памяти будет передаваться в функцию ngx_http_myutul_get_first_not_empty_value() в качестве третьего аргумента для поиска нужного элемента в массиве first_not_empty_var_data (благодаря присваиванию resvar->index значения v_idx, а  v->data - адреса v_idx_ptr в строках 448-451). В строке 452 присваиваем v->get_handler() адрес функции ngx_http_myutul_get_first_not_empty_value(), а затем возвращаем значение NGX_CONF_OK.

Хендлер ngx_http_myutil_get_first_not_empty_value() выглядит так:
225 static ngx_int_t  ngx_http_myutil_get_first_not_empty_value(
226                         ngx_http_request_t *  r, ngx_http_variable_value_t *  v,
227                         uintptr_t  data )
228 {
229     ngx_http_myutil_loc_conf_t *    lcf         = NULL;
230     ngx_http_variable_value_t *     var         = NULL;
231     ngx_array_t *                   fne_data    = NULL;
232     ngx_http_myutil_fne_elem_t *    fne_elts    = NULL;
233     ngx_http_myutil_fne_elem_t *    fne_elem    = NULL;
234     ngx_array_t *                   exprarray   = NULL;
235     ngx_http_myutil_var_handle_t *  expr        = NULL;
236     ngx_int_t *                     index       = NULL;
237     ngx_uint_t                      found_index = ( ngx_uint_t )NGX_ERROR;
238 
239     ngx_uint_t                      i;
240 
241     if ( ! data )
242         return NGX_ERROR;
243 
244     lcf = ngx_http_get_module_loc_conf( r, ngx_http_myutil_module );
245 
246     fne_data = &lcf->first_not_empty_var_data;
247     fne_elts = fne_data->elts;
248     index    = ( ngx_int_t * )data;
249 
250     for ( i = 0; i < fne_data->nelts; ++)
251     {
252         if ( *index != fne_elts[ i ].index )
253             continue;
254 
255         found_index = i;
256         break;
257     }
258 
259     if ( found_index == ( ngx_uint_t )NGX_ERROR )
260         return NGX_ERROR;
261 
262     fne_elem = &fne_elts[ found_index ];
263 
264     exprarray = &fne_elem->data;
265     expr      = exprarray->elts;
266 
267     for ( i = 0; i < exprarray->nelts; ++)
268     {
269         ngx_http_variable_value_t *  found = NULL;
270 
271         if ( expr[ i ].index == NGX_ERROR )
272         {
273             v->len          = expr[ i ].key.len;
274             v->data         = expr[ i ].key.data;
275             v->valid        = 1;
276             v->no_cacheable = 0;
277             v->not_found    = 0;
278 
279             return NGX_OK;
280         }
281 
282         found = ngx_http_get_indexed_variable( r, expr[ i ].index );
283 
284         if ( found && found->len > 0 )
285         {
286             var = found;
287             break;
288         }
289     }
290 
291     if ( ! var )
292         return NGX_ERROR;
293 
294     v->len          = var->len;
295     v->data         = var->data;
296     v->valid        = 1;
297     v->no_cacheable = 0;
298     v->not_found    = 0;
299 
300     return NGX_OK;
301 }
Здесь все относительно просто. В переменную index записываем значение третьего аргумента функции (значение, которое мы предусмотрительно сохранили в ngx_http_myutil_first_not_empty()) - строка 248. В строках 250-257 ищем соответствующий элемент в массиве first_not_empty_var_data. На найденный элемент будет указывать переменная fne_elem (строка 262). Получаем элементы массива data из fne_elem (строки 264-265). Далее проходим по всем элементам. Если текущий элемент переменная, то получаем ее значение по ее индексу (строка 282). В строках 284-288 проверяем, пусто ли это значение: если пусто, то продолжаем поиск, иначе выходим из цикла и присваиваем переменной v найденное значение (строки 294-298). Если же текущий элемент - литерал, то просто берем его значение key и присваиваем переменной v (строки 271-280; заметьте, что пустой литерал '' тоже сработает, так как проверка на expr[ i ].key.len не производится!).

Ну вот и все. Осталось запустить элементарную проверку. Будем проверять на следующей конфигурации:
events {
    worker_connections  1024;
}

http {
    server {
        listen          8010;
        server_name     router;

        location /test_first_not_empty.html {
            myutil_first_not_empty $fne_1 $arg_a $arg_b default;
            myutil_first_not_empty $fne_2 $arg_aa $arg_bb 'no data';
            if ($arg_c) {
                myutil_first_not_empty $fne_1 $arg_a $arg_c;
                echo $fne_1;
                echo $fne_2;
                break;
            }
            echo $fne_1;
            echo $fne_2;
        }
    }
}
Как видим, внутри блока if в локейшне /test_first_not_empty.html переменная $fne_1 переопределена. Нам нужно проверить, действительно ли она будет переопределена. Мы в праве этого ожидать, поскольку элементы массива first_not_empty_var_data родительской конфигурации добавляются после элементов дочерней конфигурации в функции ngx_http_myutil_merge_loc_conf(). Проверяем:
$ curl 'http://localhost:8010/test_first_not_empty.html?b=HiB'
HiB
no data
$ curl 'http://localhost:8010/test_first_not_empty.html?b=HiB&c=HiC'
HiC
no data
Действительно, присутствие аргумента c в строке запроса привело к тому, что сработала директива myutil_first_not_empty $fne_1 ... внутри if. Еще пара проверок:
$ curl 'http://localhost:8010/test_first_not_empty.html?bb=HiBB&a=HiA&b=HiB'
HiA
HiBB
$ curl 'http://localhost:8010/test_first_not_empty.html?aa=907'
default
907
Исходники модуля вместе с тестовой конфигурацией можно взять здесь.

суббота, 4 августа 2012 г.

MATE и Compiz в Fedora 17

Наконец-то после почти года вынужденных мучений с Gnome 3 мне удалось вернуться на старый добрый Gnome 2 (пусть и с другим названием MATE) в придачу с Компизом, и это в Fedora 17! Как известно, какие-то нехорошие товарищи предложили напрочь выпилить из Fedora 17 Компиз дабы убрать основного конкурента Gnome 3 (конечно же, это мое личное видение ситуации и в Fedora объясняют это другими мотивами), но вот нашлись добрые волшебники и создали репозиторий с Gnome 2 / MATE, Compiz и Emerald, дабы все те, кто до этого годами пользовался этими компонентами, могли вернуться в привычную среду, не убегая из столь же привычного дистрибутива. Ну, на самом деле, я конечно же не знаю их мотивов, но надеюсь, что развитие репозитория не остановится. Информацию я нашел случайно на этом форуме, (см. также здесь). Как советуют волшебники, сперва выполняем две команды:
yum install https://dl.dropbox.com/u/49862637/Mate-desktop/fedora_17/mate-desktop-fedora-updates/noarch/mate-desktop-release-17-2.fc17.noarch.rpm
yum groupinstall MATE-Desktop
После установки MATE можно установить Компиз. Для этого в файле /etc/yum.repos.d/Mate-Desktop_fedora_17-new-application-testing.repo заменяем строку
enabled=0
в настройке [mate-desktop-fedora-new-application-testing] на
enabled=1
Теперь устанавливаем Компиз и Эмеральд:
yum install compiz emerald ccsm compiz-mate compiz-plugins-extra compiz-plugins-extra-mate compiz-plugins-main-mate compiz-plugins-unsupported compiz-plugins-unsupported-mate emerald-themes fusion-icon
Теперь самое время перелогиниться, не забыв поменять сессию на MATE. После загрузки MATE следует настроить привычный интерфейс (конфигурационные файлы MATE отличаются от файлов Gnome 2, поэтому если у вас остались настройки Gnome 2, то толку от них немного - проще заново настроить MATE). Теперь надо проверить Компиз - запускаем fusion-icon. Кликаем правой кнопкой мыши по значку fusion-icon и выбираем Compiz в качестве Window Manager и Emerald в качестве Window Decorator. Настройки Emerald у меня сохранились c прошлых лет, а вот Компиз пришлось настраивать заново (для этого можно в fusion-icon кликнуть на Settings Manager или просто вызвать из терминала команду ccsm). Для того, чтобы fusion-icon запускался автоматически при логине в MATE, можно зайти в меню Система -> Параметры на верхней панели и кликнуть на Запускаемые Приложения. В открывшемся меню во вкладке Автоматически Запускаемые Программы добавляем новый элемент со значением Команда равным fusion-icon. Но... пока лучше не делайте этого, дальше мы напишем простой скрипт, который будет запускать компоненты, зависимые от Компиз мягко, без видимых изменений отрисовки, связанных с перезагрузкой композитного менеджера.

Это еще не все. Раньше я использовал avant-window-navigator - док-бар в нижней части дисплея. К сожалению, этот компонент был также выпилен из репозиториев Fedora, но зато я опробовал cairo-dock из репозитория rpmfusion и нашел его вполне неплохой заменой. Еще одна интересная программа - screenlets, которую можно использовать для раскидывания по экрану разных полезных штуковин - скринлетов. Ее пришлось скачать отсюда и собрать из исходников самому. К счастью, это не сложно, однако в процессе установки пришлось доустановить несколько пакетов среди которых python-BeautifulSoup и gnome-python2-libwnck.

Результат изысканий представлен на следующей картинке (картинка кликабельна). Он почти полностью (наверное только за исключением avant-window-navigator, замененного на cairo-dock) соответствует тому, что у меня было на Fedora 14.


Теперь вернемся к упомянутому скрипту для запуска fusion-icon (и не только) при входе в систему. Зачем он нужен? Видите черненькую полоску на картинке внизу экрана? Это cairo-dock. Если его запустить в некомпозитном режиме, то вместо этой красивой полоски мы увидим черный непрозрачный квадрат с непрозрачными иконками. Кто у нас запускает композитный менеджер (то бишь Компиз)? Правильно, fusion-icon. Кто запускает fusion-icon? MATE. И это настраивается в диалоге Автоматически Запускаемые Программы, там же, где настраивается запуск cairo-dock. Так вот, к сожалению Gnome 2 / MATE не гарантирует последовательность запуска этих программ и мы на какое-то мгновение можем увидеть "черный квадрат" вместо нормального cairo-dock при входе в систему. Чтобы загрузка Компиз выглядела мягкой, мы будем запускать все программы, отрисовка которых зависит от наличия композитного менеджера, внутри специального скрипта StartCompizSession после гарантированного старта Компиз. Этот скрипт мы поместим в качестве нового элемента в Автоматически Запускаемые Программы, а программы, зависящие от присутствия композитного менеджера, соответственно оттуда удалим. Помимо cairo-dock сюда относятся различные скринлеты.
#!/bin/bash

fusion-icon &
sleep 1

nmbOfTry=1
nmbOfTryLimit=20

while (( $nmbOfTry < $nmbOfTryLimit )) ; do
  ps -U $USER -o args | grep '^emerald' > /dev/null
  (( $? == 0 )) && break
  (( ++$nmbOfTry ))
  sleep 1
done

if (( $nmbOfTry < $nmbOfTryLimit )) ; then
  #avant-window-navigator &
  cairo-dock -o &

  # screenlets daemon
  screenlets-daemon &

  # various screenlets
  python -u /usr/share/screenlets/PerfectClock/PerfectClockScreenlet.py &
  python -u /usr/share/screenlets/DiskSpace/DiskSpaceScreenlet.py &
  python -u /usr/share/screenlets/ClearCalendar/ClearCalendarScreenlet.py &
  python -u /usr/share/screenlets/Slideshow/SlideshowScreenlet.py &
  python -u /usr/share/screenlets/aSensors/aSensorsScreenlet.py &
fi

воскресенье, 29 апреля 2012 г.

vim: автоматическое переключение раскладки клавиатуры при входе и выходе из режима ввода

Если нужно писать в vim что-то на русском языке, то одной из самых раздражающих неприятностей является необходимость постоянного ручного переключения раскладки клавиатуры на латинскую при выходе из режима ввода и обратно на русскую - при входе в режим ввода (даже одна эта фраза уже раздражает). Оказывается, можно очень просто автоматизировать этот процесс, во всяком случае в среде под управлением X server. Волшебным средством автоматизации будет использование внешней программы, которая умеет возвращать значение текущей раскладки и устанавливать новую. При поиске такого волшебного средства я опробовал xte из пакета программ xautomation и xdotool. Эти программы можно установить прямо из репозитория моего дистрибутива (что, конечно же, плюс), но они не умеют выполнять первую задачу, поэтому на роль волшебного средства не годятся. Поискав еще немного я нашел то, что надо: программу xkb-switch. Я не буду описывать ее опции, они достаточно просты, а сразу приведу код, который нужно добавить в .vimrc, чтобы автоматическое переключение на латиницу при выходе из режима ввода заработало:
fun<SID>xkb_switch(mode)
    let cur_layout = substitute(system('xkb-switch')'\n''''g')
    if a:mode == 0
        if cur_layout != 'us'
            call system('xkb-switch -s us')
        endif
        let b:xkb_layout = cur_layout
    elseif a:mode == 1
        if exists('b:xkb_layout') && b:xkb_layout != cur_layout
            call system('xkb-switch -s '.b:xkb_layout)
        endif
    endif
endfun

if executable('xkb-switch')
    autocmd InsertEnter * call <SID>xkb_switch(1)
    autocmd InsertLeave * call <SID>xkb_switch(0)
endif
Все интересное происходит в функции xkb_switch(mode), которая вызывается с разными аргументами mode при выходе из режима ввода (значение mode=0) и входе в режим ввода (значение mode=1). В первом случае мы вызываем системную команду xkb-switch -s us, которая принудительно переводит раскладку клавиатуры в латиницу, запоминая какая раскладка была на выходе из режима ввода в буферной переменной b:xkb_layout. Во втором случае (при входе в режим ввода) проверяется, существует ли переменная b:xkb_layout, и если это так, то вызывается xkb-switch c опцией -s, в которую передается значение этой переменной. Это позволяет восстановить раскладку клавиатуры, установленную в данном буфере перед выходом из режима ввода. Я еще раз хочу подчеркнуть: в данном буфере! То есть раскладка устанавливается для каждого буфера такой, которой она была при выходе из режима ввода именно в нем, а это очень удобно.

Update. Небольшое дополнение для тех, кто пользуется режимом выделения текста (Select mode). Это очень удобный режим, когда текст сначала выделяется, а затем поверх него пишется новый текст (аналог классического режима выделения текста в офисных редакторах). Перейти в него из нормального режима vim можно, набрав gh (нормальный режим выделения), gH (строчный режим выделения) или g<C-h> (блочный режим выделения). Кроме того, можно переключаться между визуальным режимом и режимом выделения текста с помощью <C-g>, а также настроить vim таким образом, чтобы выделение текста мышью переводило его в режим выделения текста (вместо визуального режима по умолчанию).

Поскольку я иногда применяю режим выделения текста, то быстро обнаружил, что при его использовании, автоматическое переключение раскладки ведет себя неподобающе, а именно: если ожидается переключение на кириллицу, то первый введенный символ остается латинским, а следующие, как и ожидалось, печатаются кириллицей. Это легко объясняется тем, как работает режим выделения текста: вначале vim проверяет, является ли вводимый символ печатным, затем, если это так, этот символ вводится в начале выделенного текста, а уже после этого vim переходит в режим ввода. Очевидно, что первый введенный символ останется латинским, так как событие InsertEnter в момент его ввода еще не наступило.

Для того, чтобы исправить это недоразумение, я добавил новую функцию xkb_mappings_load(), которая переопределяет вышеперечисленные команды gh, gH, g<C-h> и <C-g>, с помощью маппингов, которые вызывают функцию xkb_switch() с нужным аргументом mode. Сразу хочу отметить, что проблема останется для тех, кто использует мышь для перехода в режим выделения текста (вы можете добавить аналогичный маппинг для этого случая сами), а также в случае выхода из режима выделения с помощью клавиши <Esc> (дело в том, что xkb_switch() может перевести раскладку в кириллицу, выход же из режима выделения текста с помощью <Esc> минует режим ввода, а следовательно мы можем оказаться в нормальном режиме с кириллической раскладкой).
fun<SID>xkb_mappings_load()
    for hcmd in ['gh''gH''g^H']
        exe "nnoremap <buffer> <silent> ".hcmd.
                    \ " :call <SID>xkb_switch(1)<CR>".hcmd
    endfor
    xnoremap <buffer> <silent> <C-g> :<C-u>call <SID>xkb_switch(1)<CR>gv<C-g>
    snoremap <buffer> <silent> <C-g> <C-g>:<C-u>call <SID>xkb_switch(0)<CR>gv
    let b:xkb_mappings_loaded = 1
endfun

fun<SID>xkb_switch(mode)
    let cur_layout = substitute(system('xkb-switch')'\n''''g')
    if a:mode == 0
        if cur_layout != 'us'
            if !exists('b:xkb_mappings_loaded')
                call <SID>xkb_mappings_load()
            endif
            call system('xkb-switch -s us')
        endif
        let b:xkb_layout = cur_layout
    elseif a:mode == 1
        if exists('b:xkb_layout') && b:xkb_layout != cur_layout
            call system('xkb-switch -s '.b:xkb_layout)
        endif
    endif
endfun
Обратите внимание: ^H в первой строке функции xkb_mappings_load() не является записью двух символов ^ и H! Это один символ, который соответствует <Bs> в таблице ASCII. Для того, чтобы ввести его в vim, следует в режиме ввода набрать <C-v>008.

Функция xkb_mappings_load() вызывается из xkb_switch() только в случае необходимости, то есть когда при выходе из режима ввода раскладка клавиатуры не является латинской (точнее us), и только для текущего буфера. Почему я просто не замапил эти команды в глобальной области, например рядом с автокомандами InsertEnter и InsertLeave? Все просто: маппинги <C-g> в визуальном режиме с использованием вызова команд в нормальном режиме не даются даром: заметили использование команды gv внутри маппингов, которая говорит: выдели-ка потерянное выделение заново? Потеря и восстановление области выделения приводят к перерисовке текста на экране. Поскольку редактировать русские тексты приходится очень нечасто, то глобальное определение маппингов для <C-g> ухудшило бы юзабилити vim без необходимости.

Update 2. По каким-то причинам вызов функции system() из autocmd InsertEnter может иногда приводить к появлению мусора в строке ввода если vim стартовал в режиме ввода (например был запущен командой vim -c start или, при наличии плагинов c.vim или perl-support.vim, пользователь открыл для редактирования новый C/C++ или Perl файл и соответствующий плагин вставил хедер с переводом vim в режим ввода). Ниже приводится вариант функции xkb_switch(), в котором system() не вызывается при первом переводе буфера в режим ввода, а также добавлен комментарий с упоминанием этого неприятного феномена.
fun<SID>xkb_switch(mode)
    if a:mode == 0
        " by some reason calling system() function may produce garbage on
        " display if vim started in Insert mode (e.g. when c-support,
        " perl-support etc. apply file header for a new file), so definition
        " of cur_layout was moved inside if- and else- blocks from top level
        let cur_layout = substitute(system('xkb-switch')'\n''''g')
        if cur_layout != 'us'
            if !exists('b:xkb_mappings_loaded')
                call <SID>xkb_mappings_load()
            endif
            call system('xkb-switch -s us')
        endif
        let b:xkb_layout = cur_layout
    elseif a:mode == 1
        if exists('b:xkb_layout')
            let cur_layout = substitute(system('xkb-switch')'\n''''g')
            if b:xkb_layout != cur_layout
                call system('xkb-switch -s '.b:xkb_layout)
            endif
        endif
    endif
endfun
Update 3. См. важное обновление здесь.