Показаны сообщения с ярлыком xkb-switch. Показать все сообщения
Показаны сообщения с ярлыком xkb-switch. Показать все сообщения

понедельник, 16 декабря 2019 г.

Новый переключатель раскладки g3kb-switch для Gnome 3 и vim-xkbswitch

Люди говорят (а я проверил), что xkb-switch больше (и наверное уже давно) не работает в Gnome Shell. Поэтому я решил написать его аналог для этой популярной среды. Он называется g3kb-switch и ведет себя практически аналогично xkb-switch. Так же, как и xkb-switch, он может быть настроен для работы с vim-xkbswitch. В базовом варианте для этого нужны всего две строки в файле .vimrc.
let g:XkbSwitchEnabled = 1
let g:XkbSwitchLib = '/usr/local/lib/libg3kbswitch.so'
Реализация g3kb-switch сильно отличается от xkb-switch: он не работает на уровне X протокола, а выполняет удаленные действия в Gnome Shell с помощью синхронных вызовов, передаваемых через шину сообщений D-Bus.

воскресенье, 10 февраля 2013 г.

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

Это не автоматическое переключение раскладки клавиатуры при входе и выходе из режима ввода, о котором я писал здесь и здесь. Это гораздо более интересная, хотя и не настолько важная, как предыдущая, функция. Представьте, что вы редактируете некий файл, и вам нужно переключать раскладку клавиатуры в зависимости от положения курсора, не выходя из режима ввода. Я не взял эту задачу с потолка. Мне понадобилась такая функция в процессе изучения немецкого языка: просто мне намного проще запоминать слова, если я их самостоятельно выписываю в таблицу с оригинальным словом или фразой в одной колонке и его/ее переводом в другой колонке. Например:
| Wort         | Übersetzung |
|--------------|-------------|
| der Mond     | луна        |
| humpeln      | хромать     |
| stark        | сильный     |
| die Bewegung | движение    |
| bewegen      | двигать     |
Представьте, сколько нужно переключений раскладки, чтобы заполнить хотя бы десять рядов, это притом, что всегда присутствует английская раскладка, которую придется каждый раз старательно пропускать. В этой статье я покажу, как сделать, чтобы раскладка клавиатуры переключалась сама, в зависимости от положения курсора в первой или второй колонке таблицы. Разумеется, это всего лишь частный пример, и предложенный алгоритм можно использовать и в иных целях, когда синтаксический формат редактируемого файла заранее известен.

Определять положение курсора будем по синтаксическому идентификатору под курсором. Соответственно, нам понадобится определить синтаксис для словарной таблицы и написать синтаксический скрипт для этого синтаксиса. Я не стал определять собственный тип файла (filetype) для словаря, а просто решил, что его типом будет vimwiki. Vimwiki очень хорошо работает с таблицами, в частности имеет отличную поддержку для навигации внутри таблицы при нажатии на клавишу табуляции, автоматического добавления строк и форматирования столбцов. Однако vimwiki не дает того, что нам нужно: синтаксического различения первого и второго столбцов, а это значит, что мы не сможем детектировать положение курсора в таблице словаря и, в виде бонуса, не сможем подсвечивать столбцы разными цветами. Что же делать? Будем рассматривать наш словарь, как синтаксическую разновидность vimwiki. Это значит, что filetype словаря будет равен vimwiki, а синтаксические особенности мы опишем в файле $HOME/.vim/after/syntax/vimwiki.vim. Для формального различения словаря и других файлов vimwiki положим, что словарь будет иметь файловое расширение .mdict. Поэтому в .vimrc добавляем строку
autocmd BufNewFile,BufRead *.mdict setlocal filetype=vimwiki | EnableXkbSwitch
(она также присутствует ниже в листинге для .vimrc). Файл $HOME/.vim/after/syntax/vimwiki.vim выглядит так:
if match(bufname('%')'\.mdict$') == -1
    finish
endif

syntax match mdictOriginal '\%(^\s*|\)\@<=[^|]\+\ze|[^-]'
            \ containedin=VimwikiTableRow contained

syntax match mdictTranslated '\%([^-]|\)\@<=[^|]\+\ze|$'
            \ containedin=VimwikiTableRow contained

hi mdictOriginalHl term=standout ctermfg=63 guifg='#d7d7ff'
autocmd ColorScheme * hi mdictOriginalHl term=standout
            \ ctermfg=63 guifg='#d7d7ff'

hi mdictTranslatedHl term=standout ctermfg=28 guifg='#d7ffd7'
autocmd ColorScheme * hi mdictTranslatedHl term=standout
            \ ctermfg=28 guifg='#d7ffd7'

hi link mdictOriginal   mdictOriginalHl
hi link mdictTranslated mdictTranslatedHl
Содержимому первого столбца таблицы соответствует регулярное выражение mdictOriginal, второго столбца - mdictTranslated. В первых строках проверяется, что файл имеет расширение .mdict, и если это не так, то скрипт сразу заканчивает работу.

А теперь код, который следует поместить в .vimrc сразу за кодом для xkb_switch (см. здесь).
" automatic keyboard layout switching in a simple dictionary in insert mode
" (filetype is a subclass of vimwiki and must have extension '.mdict';
" there must exist syntax support in dedicated script
" $HOME/.vim/after/syntax/vimwiki.vim to define matches for original and
" translated colums 'mdictOriginal' and 'mdictTranslated')
" FIXME: currently layout will not switch correctly from within select modes
fun<SID>dict_check_lang(force)
    if !executable(g:XkbSwitchLib)
        return
    endif

    let cur_synid  = synIDattr(synID(line(".")col(".")1)"name")

    if !exists('b:saved_cur_synid')
        let b:saved_cur_synid = cur_synid
    endif

    if cur_synid != b:saved_cur_synid || a:force
        let cur_layout = libcall(g:XkbSwitchLib, 'Xkb_Switch_getXkbLayout',
                    \ '')
        if b:saved_cur_synid == 'mdictOriginal'
            let b:xkb_layout_dict_orig = cur_layout
        endif
        if b:saved_cur_synid == 'mdictTranslated'
            let b:xkb_layout_dict_trans = cur_layout
        endif
        if cur_synid == 'mdictOriginal'
            if exists('b:xkb_layout_dict_orig')
                call libcall(g:XkbSwitchLib, 'Xkb_Switch_setXkbLayout',
                        \ b:xkb_layout_dict_orig)
            else
                let b:xkb_layout_dict_orig = cur_layout
            endif
        endif
        if cur_synid == 'mdictTranslated'
            if exists('b:xkb_layout_dict_trans')
                call libcall(g:XkbSwitchLib, 'Xkb_Switch_setXkbLayout',
                        \ b:xkb_layout_dict_trans)
            else
                let b:xkb_layout_dict_trans = cur_layout
            endif
        endif
        let b:saved_cur_synid = cur_synid
    endif
endfun

autocmd BufNewFile,BufRead *.mdict setlocal filetype=vimwiki | EnableXkbSwitch
autocmd BufNewFile         *.mdict VimwikiTable 2 2
autocmd BufNewFile         *.mdict exe "normal dd" | startinsert
autocmd InsertEnter        *.mdict call <SID>dict_check_lang(1)
autocmd CursorMovedI       *.mdict call <SID>dict_check_lang(0)
Теперь при открытии нового файла с расширением .mdict будет автоматически создаваться таблица размерностью 2x2. В верхней сроке нужно поместить названия столбцов. При заполнении первой строки таблицы необходимо вручную переключать требуемые раскладки клавиатуры, в дальнейшем они будут переключаться автоматически. При выходе из режима ввода будет автоматически включена английская раскладка (так как мы включили EnableXkbSwitch), при входе в режим ввода будет включена раскладка в соответствии со столбцом, в котором находится курсор. При открытии уже существующего файла словаря vim не знает о соответствии столбцов и раскладок, поэтому его придется научить снова, дважды вручную переключив раскладку в разных столбцах.

Из недостатков/недоделок нужно упомянуть неправильную работу при переключении в режим ввода из режима выделения текста (Select mode), а также отсутствие немецких дубликатов маппингов режима ввода - их можно добавить по аналогии с русскими (см. здесь) - однако, поскольку немецкая раскладка мало отличается от английской, они, скорее всего, не понадобятся вообще.

вторник, 18 декабря 2012 г.

vim: запуск xkb-switch через интерфейс libcall()

Молодцы хабрахабровцы! Придумали как запустить системное переключение раскладки клавиатуры без использования call system(). Предысторию вопроса можно изучить здесь. Интерфейс libcall() закрывает вопрос появления мусора на экране терминала. И поэтому отныне xkb-switch поддерживает libcall(), хвала гитхабу с его форками и пулл-реквестами!

В .vimrc поддержка автоматического переключения русской раскладки в режиме ввода теперь выглядит так:
" ---- Automatic keyboard layout switching upon entering/leaving insert mode
" ---- using xkb-switch utility
" ----
let g:XkbSwitchEnabled = 0
let g:XkbSwitchLib = "/usr/local/lib/libxkbswitch.so"

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>ru_mappings_load()
    redir => mappings
    silent imap
    redir END
    for mapping in split(mappings, '\n')
        let value = substitute(mapping, '\s*\S\+\s\+\S\+\s\+\(.*\)''\1''')
        " do not duplicate <script> mappings (when value contains '&')
        if match(value, '^[\s*@]*&') != -1
            continue
        endif
        let data = split(mapping, '\s\+')
        " do not duplicate <Plug> mappings (when key starts with '<Plug>')
        if match(data[1], '^\c<Plug>') != -1
            continue
        endif
        let from = 'qwertyuiop[]asdfghjkl;\\x27zxcvbnm,.`/'.
                    \ 'QWERTYUIOP{}ASDFGHJKL:\\x22ZXCVBNM<>?~@#\\x24^\\x26|'
        let to   = 'йцукенгшщзхъфывапролджэячсмитьбюё.'.
                    \ 'ЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮ,Ё\\x22№;:?/'
        " protect backslashes before next evaluations
        let newkey = substitute(data[1], '\''\\\\''g')
        " pre-evaluate the new key
        let newkey = substitute(newkey,
                    \ '\(\%(<[^>]\+>\)*\)\(.\{-}\)\(\%(<[^>]\+>\)*\)$',
                    \ '"\1".tr("\2", "'.from.'", "'.to.'")."\3"''i')
        " evaluate the new key
        let newkey = eval(newkey)
        " do not reload existing mapping unnecessarily
        if newkey == data[1]
            continue
        endif
        let mapcmd = match(value, '^[\s&@]*\*') == -1 ? 'imap' : 'inoremap'
        " probably the mapping was defined using <expr>
        let expr = match(value,
                    \ '^[\s*&@]*[a-zA-Z][a-zA-z0-9_#\-]*(.\{-})$') != -1 ?
                    \ '<expr>' : ''
        " new maps are always silent and buffer-local
        exe mapcmd.' <silent> <buffer> '.expr.' '.newkey.' '.
                    \ maparg(data[1], 'i')
    endfor
endfun

fun<SID>xkb_switch(mode)
    let cur_layout = libcall(g:XkbSwitchLib, 'Xkb_Switch_getXkbLayout''')
    if a:mode == 0
        if cur_layout != 'us'
            call libcall(g:XkbSwitchLib, 'Xkb_Switch_setXkbLayout''us')
        endif
        let b:xkb_layout = cur_layout
    elseif a:mode == 1
        if !exists('b:xkb_mappings_loaded')
            call <SID>xkb_mappings_load()
            call <SID>ru_mappings_load()
        endif
        if exists('b:xkb_layout')
            if b:xkb_layout != cur_layout
                call libcall(g:XkbSwitchLib, 'Xkb_Switch_setXkbLayout',
                            \ b:xkb_layout)
            endif
        endif
    endif
endfun

fun<SID>enable_xkb_switch(force)
    if g:XkbSwitchEnabled && !a:force
        return
    endif
    if executable(g:XkbSwitchLib)
        autocmd InsertEnter * call <SID>xkb_switch(1)
        autocmd InsertLeave * call <SID>xkb_switch(0)
    endif
    let g:XkbSwitchEnabled = 1
endfun

command EnableXkbSwitch call <SID>enable_xkb_switch(0)

if g:XkbSwitchEnabled
    call <SID>enable_xkb_switch(1)
endif
Обратите внимание: по умолчанию автоматическое переключение раскладки неактивно (переменная g:XkbSwitchEnabled равна 0). Пользователь может включить ее либо вручную (введя команду :EnableXkbSwitch), либо объявив автокоманды, которые сработают при определенных условиях. Например, можно включить автоматическое переключение раскладки для файлов типа reStructuredText и tex:
autocmd FileType rst,tex EnableXkbSwitch
В новой версии скрипта я добавил функцию ru_mappings_load(). Это очень полезная функция - она создает русифицированные дубликаты всех имеющихся маппингов режима ввода и загружает их в локальный буфер. Например в плагине riv, который я использую для редактирования файлов reStructuredText, есть удобный маппинг <C-E>l` для переключения режима списка во время редактирования файла. Если бы нам пришлось переключать раскладку клавиатуры для ввода латинских символов, используемых в таких маппингах, то прелесть использования нашего подхода если бы и не улетучилась вовсе, то все же серьезно пострадала. Теперь же русифицированный дубликат этого маппинга <C-E>дё загружается автоматически и позволяет не беспокоится более о переключении раскладки клавиатуры.

Внимание: алгоритм трансляции из латинского ключа в русский не универсален и возможны сбои для необычных ключей! Однако для всех маппингов riv, а также маппингов плагина c.vim он работает исправно. Список загруженных маппингов режима ввода можно просмотреть командой :imap.

Update. Оформили в виде плагина, см. здесь и здесь. Последнее замечание относительно неуниверсальности трансляции маппингов в новом плагине неактуально.

воскресенье, 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. См. важное обновление здесь.