воскресенье, 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), а также отсутствие немецких дубликатов маппингов режима ввода - их можно добавить по аналогии с русскими (см. здесь) - однако, поскольку немецкая раскладка мало отличается от английской, они, скорее всего, не понадобятся вообще.

среда, 16 января 2013 г.

Проблемы, с которыми я столкнулся после обновления Fedora 17 до Fedora 18

У меня установлены MATE и Compiz отсюда. В репозитории пока нет обновленных пакетов для Fedora 18. Это приводит к нескольким проблемам, которые я перечислю в порядке значимости.
  1. Симптом: caja (аналог nautilus в MATE) не может загрузить иконки для файлов (в т.ч. на рабочем столе) и начинает поедать 100% CPU.
    Лечение: удаляем директорию $HOME/.thumbnails и создаем вместо нее символическую ссылку на $HOME/.cache/thumbnails:
    cd
    rm -r .thumbnails/
    ln -s .cache/thumbnails .thumbnails
    
    Если целевая директория $HOME/.cache/thumbnails отсутствует, то создайте ее. Проблема уже исправлена в последних версиях mate-desktop ветки 1.4.

  2. Симптом: команда yum check показывает отсутствующие зависимости для некоторых компонентов MATE, в частности atril-libs и mate-color-manager требуют libtiff.so.3, а mate-note - libpcre.so.0. Эти библиотеки в Fedora 18 были обновлены до новых версий.
    Лечение: просто забить (однако в этом случае придется удалить иконку mate-note с панели MATE).
    Вариант: скачать SRPM файлы соответствующих библиотек из Fedora 17 (например отсюда), установить их с помощью rpm (я обычно делаю это от имени специального пользователя mockbuild):
    rpm -Uvh libtiff-3.9.7-1.fc17.src.rpm
    
    (здесь я привожу пример только для libtiff). Перейти в директорию rpmbuild/SPECS, заменить строку Name: libtiff на Name: libtiff3 и собрать rpm:
    rpmbuild -ba libtiff.spec
    
    Теперь перейти в директорию ../RPMS/<your-arch> и от имени суперпользователя установить новый пакет libtiff3:
    rpm -Uvh libtiff3-3.9.7-1.fc18.R.x86_64.rpm
    
Вообще-то я надеюсь, что данный репозиторий MATE скоро обновится до Fedora 18 и первые две проблемы уйдут.

Теперь более интересная проблема. Сообщение Authentication is required for powering off while other users are logged in и предложение ввести пароль суперпользователя при попытке выключить или перезагрузить компьютер из GDM или пользовательской сессии. Возможно у вас не возникнет этой проблемы, или она будет возникать иногда при попытке выключить компьютер из GDM сразу же после выхода из пользовательской сессии.

У меня эта проблема проявлялась всегда до тех пор, пока я не поправил... что бы вы думали? ...инит-скрипт для демона TurboPrint (есть такая замечательная программа для печати, хотя и не бесплатная; настоятельно рекомендую особенно для печати фотографий). У меня установлена старая версия TurboPrint 2.15. Возможно в новых версиях проблема с systemd уже решена, но для обновления TurboPrint придется заплатить. Поскольку установленная версия меня вполне устраивает, то пришлось править скрипты для нее.

Итак, по порядку. Как обнаружить причину такой странной и специфичной для Fedora 18 проблемы? Что мы имеем? Мы загрузили компьютер, пытаемся тут же его выключить и получаем сообщение, что мы не одни в системе! Оказывается, в новой Fedora за сессиями пользователей следит systemd, а просмотреть состояние сессий можно с помощью утилиты loginctl. Так и делаем. Переходим в виртуальную консоль, логинимся от имени рута и набираем loginctl list-sessions. На экран выводится
   SESSION        UID USER             SEAT            
        c1          4 lp
Супер! Пользователь lp в системе! Что же он делает?
# loginctl user-status lp
lp (4)
           Since: Tue, 2013-01-15 21:26:02 MSK; 3min 49s ago
           State: active
        Sessions: c1
          CGroup: name=systemd:/user/lp
                  └ c1
                    └ 701 /usr/bin/tprintdaemon 0
Очевидно, это результат запуска системного демона /etc/init.d/tpdaemon, который при старте открывает оболочку для пользователя lp. Как теперь быть? Мигрировать демон на systemd! Здесь я нашел как это сделать. Сначала отключаем демон в init.d:
chkconfig tpdaemon off
service tpdaemon stop
Затем создаем новый сервисный файл для systemd /etc/systemd/system/tprintdaemon.service и записываем в него строки
[Unit]
Description=TurboPrintDaemon

[Service]
Type=forking
ExecStart=/usr/bin/tprintdaemon
Restart=on-abort

[Install]
WantedBy=multi-user.target
Теперь регистрируем и запускаем новый сервис:
systemctl enable tprintdaemon.service
systemctl start tprintdaemon.service
Как это обычно бывает, с первого раза новый подход не принес результата. Выяснилось, что TurboPrint запускает программу turboprint-monitor 0 -hide, которая работает от имени залогинившегося пользователя и действует как демон, что не позволяет systemd и loginctl считать, что этот пользователь успешно разлогинился. Выход из ситуации: убивать turboprint-monitor при выходе пользователя из графической оболочки, благо эта программа запускается при каждом новом логине заново. Как это сделать? Я использую в качестве дисплейного менеджера GDM. В GDM есть прекрасное место, где можно определить действия при выходе из пользовательской сессии: это файл /etc/gdm/PostSession/Default. Вот строки, которые я добавил в этот файл для принудительного завершения turboprint-monitor, а также pulseaudio:
for service in pulseaudio turboprint-monitor ; do
  PID=$(loginctl user-status $USER | grep "[0-9]\+\s\+\S*$service" | \
                                     sed 's/^[^0-9]\+\([0-9]\+\).*/\1/')
  [ "$?" == "0" ] && kill $PID
done
Зачем здесь pulseaudio? Оказывается, он большой тормоз и выходит по завершении пользовательской сессии не сразу. Это приводит к задержке очистки пользовательской сессии, и соответственно возможен сценарий, когда пользователь выходит из графической сессии и, сразу пытаясь выключить компьютер из GDM, получает то же невнятное сообщение о присутствии других пользователей с предложением ввести пароль рута.

В общем, systemd в роли менеджера сессий вызывает впечатление пока еще сырого продукта.

Update. Репозиторий MATE обновился! Инструкция по установке здесь. Соответственно проблема с зависимостями и mate-note уходит. Однако автор решил не обновлять сами пакеты до выхода стабильной ветки MATE 1.6, которая ожидается в марте этого года, а это значит, что проблема с caja скорее всего не исправлена. Кроме того, после обновления я заметил, что цвет текста в верхней панели стал почти полностью сливаться с фоном панели, поэтому пришлось добавить в файл $HOME/.gtkrc-2.0 такие строки:
style "my_color"
{
        fg[NORMAL] = "#AAAAAA"
}
widget "*PanelWidget*" style "my_color"
widget "*PanelApplet*" style "my_color"
Кроме того, я убрал с панели стандартный индикатор раскладки клавиатуры, так как непонятно, как изменить его цвет, да и к тому же он мне не нужен, так как я использую gxneur:
mateconftool-2 -s /desktop/mate/peripherals/keyboard/general/disable_indicator -t bool true

Update 2. Еще одна проблема. Программа epstopdf стала генерировать пустые pdf с такими сообщениями об ошибке:
Error: /invalidfont in /findfont
Operand stack:
   Times-Roman
Execution stack:
   ...
Dictionary stack:
   ...
Current allocation mode is local
Last OS error: Not a directory
GPL Ghostscript 9.06: Unrecoverable error, exit code 1
Выяснилось, что это из-за того, что я установил пакет fontconfig-infinality, который банит шрифты Type 1 в /etc/fonts/infinality/infinality.conf. Чтобы восстановить работу epstopdf, следует закомментировать в этом файле следующие строки:
    <selectfont>
        <rejectfont>
            <pattern>
                <patelt name="fontformat" >
                    <string>Type 1</string>
                </patelt>
            </pattern>
        </rejectfont>
    </selectfont>
Очевидно, данная проблема связана не с обновлением Fedora, а, собственно, с установкой fontconfig-infinality.
 
Update 3. Автор репозитория MATE таки отказался поддерживать свой репозиторий для Fedora 18. То, что он выложил раньше, оказалось его ошибкой (см. здесь). Так что теперь у нас два варианта (напомню, что моим приоритетом является сохранение Compiz в системе, а этот пакет предоставляется только данным репозиторием).
  1. Оставить все как есть. В этом случае все пакеты из старого репозитория MATE, включая Compiz, останутся в системе и будут работать. Если вы не успели проапгрейдить MATE из ошибочного репозитория, то останутся проблемы с зависимостями (libtiff.so.3 и libpcre.so.0), и, вполне возможно, в дальнейшем появятся новые неудовлетворенные зависимости, так как вы, очевидно, не сможете проапгрейдить пакеты MATE из более несуществующего репозитория.

  2. Выбрать и снести выборочные пакеты из старого репозитория MATE c помощью rpm -evh --nodeps. При этом не следует удалять пакеты Compiz, поскольку автор репозитория обещал добавить их в новый репозиторий mate-desktop-extra через неделю, и пока их там нет! Собственно для этого и нужен флаг --nodeps для rpm, так как пакеты Compiz зависят от MATE. После удаления старых пакетов MATE нужно установить новые пакеты командой
    yum groupinstall MATE-desktop
    
    и добавить пакеты из репозитория mate-desktop-extra:
    yum install https://dl.dropbox.com/u/105479527/Mate-Desktop/fedora-release-extra-18/mate-desktop-fedora/noarch/mate-desktop-extra-release-18-1.fc18.noarch.rpm
    yum groupinstall mate-desktop-extra
    
    Минус этого подхода в сложности выбора нужных пакетов для удаления и необходимости заново настраивать внешний вид десктопа. Плюсы: отсутствие проблем с зависимостями и постоянные обновления пакетов из стандартного репозитория Fedora.
Лично я воспользовался вторым способом. Все работает, но есть и регрессии, связанные с верхней панелью: по какой-то причине отсутствует апплет для датчиков температуры, нет больше mintmenu (оно якобы несовместимо с новой версией панели), нет апплета переключения пользователей, погодный апплет почему-то выводит текст Неизвестно.
 
Update 4. До Fedora 18 я пользовался этим репозиторием для texlive. Теперь все пакеты из последнего texlive есть в стандартном репозитории, поэтому сносим все пакеты из старого репозитория и устанавливаем их из нового:
rpm -qa tex-* texlive-* | xargs yum -y remove
yum install texlive texlive-collection-langcyrillic texlive-disser texlive-mhchem texlive-braket texlive-feynmf texlive-hepparticles texlive-hepnames texlive-subfigure texlive-wrapfig texlive-gost texlive-bibtexu texlive-helvetic texlive-epstopdf texlive-metapost texlive-minted texlive-latexmk texlive-cm-super
В списке на установку обязательными являются только два пакета: texlive и texlive-collection-langcyrillic. Остальные пакеты из списка не устанавливаются по умолчанию, но я их использую. Кстати, проблему с переносами русских слов, о которой я писал здесь, в новом репозитории кажется уже решили.

четверг, 10 января 2013 г.

A perl script for gathering blogger stats and showing it on a terminal

I decided to experiment and write this post in English. Recently I uploaded a small perl script bloggerstats on the github (see here). The script can help to gather statistics of a blog hosted on the blogger.com. The stats can be dumped on a terminal or redirected to a file. As well pretty charts in PNG format can be created. See images below.


The usage of the script is shown with -h or --help option. The script requires several mandatory and optional perl modules: JSON, URI::Escape, Encode, Data::Dumper (for dumping received JSON data in debug mode), Text::TabularView (for printing result in pretty tables), Chart::Bars and Time::Local (for making charts). All modules are either normally shipped with perl distributions or available in the system repository (I found all of them in my standard Fedora 17 rpm repository). The script also requires several external programs: sed, curl (for network communication with blogger.com) and ngrep (for sniffing sensitive data required by blogger.com, see below).

To start using it one should provide basic configuration settings in file $HOME/.bloggerstatrc. I put a template file bloggerstatrc.tmpl in the github repository to start with. Here is its content:
$blogID      = 'put-here-your-blog-id';
$bloghost    = 'blogger.com';

$statsurl    = "http://www.$bloghost/blogger_rpc?blogID=$blogID";

$start_year  = 2010;
$start_month = 5;       # months start from 1, so January is 1

# AUTOGENERATED (do not delete this line!)
Actually .bloggerstatrc is an ordinary perl source file and is to be sourced from bloggerstats, so you can put there any statements that perl can compile, but those 5 variables defined in this template are mandatory ones. Of course you have to substitute your real blogID instead put-here-your-blog-id (this is the value of the parameter blogID in any related HTTP GET request to blogger.com: you can find it in the address line in your browser). You also have to substitute proper values of $start_year and $start_month: they must correspond to the first bin of the all-time visits chart in your blog. The line with comment starting with AUTOGENERATED must reside below your regular settings: it is used by the sniffer for updating other sensitive variables (cookies, headers and xsrf token) below it. Every time you run bloggerstats -w all content below this line gets removed!

Why do I say here about sniffer? Unfortunately blogger.com API seems to be closed at the moment. The only thing known is that it uses GWT (Google Web Toolkit) and JSON for sending data to the client. GWT produces very large obfuscated Javascript code which finally creates xsrf token and sends it to the server. The algorithm that creates it is unknown. The generated token is valid through approximately 24 hours. Besides the token blogger.com checks for sessionID cookies (which can also become invalid but not so often as the token) and GWT related HTTP headers. All these data are collected by the sniffer and put below the comment line starting with AUTOGENERATED in .bloggerstatsrc. As soon as sniffing network interfaces requires root privileges the script must possess them. To achieve this login as root, open some new sudoers file (say /etc/sudoers.d/users) with visudo and put there following lines
Cmnd_Alias       NETTASKS = /usr/sbin/ngrep
<your-login-id>  ALL = NOPASSWD: NETTASKS
where <your-login-id> is your system login name. Word NOPASSWD in the second line is important: it prompts sudo do not ask password when starting ngrep.

So now that you configured basic parameters in .bloggerstatsrc you may want to launch the sniffer by running bloggerstatrc -w in a terminal (it will wait until stats request is sent to blogger.com via a browser), open your blogger.com stats page in a browser or just refresh it. The sniffer must exit (though it may fail to exit at very heavy network load: refresh stats page several times in this case) and now bloggerstats is ready to gather blogger statistics. As soon as saved xsrf token or cookies become invalid (in one day or so) you will see errors, running the sniffer once again will bring bloggerstats back to life.

Finally I want to show settings for tabular highlights as seen on the first image above (look here about hl and Term::Highlight). File .hlrc:
snippet bstats  -b -82 '^(?:\+-+)+\+$' '^\|\s+' '\s+\|$' \
                '\s+\|\s+(?=.*\s+\|$)'-67 \
                '(?<=^\|)\s+(?:Overview|Page|Keyword|Site|URL|Country)\s+' \
                '(?<=^\|)\s+(?:Browser|OS)\s+' -215 '(?<=\|)\s+\d+\s+(?=\|$)' \
                -rb -48 '(?<=^\|)\s+Today\s+(?=\|)'
File .hl_functions:
function bloggerstats
{
    `env which bloggerstats` $@ | hl -sbstats
}
Vielleicht schreibe ich nächstes Mal auf Deutsch :)

вторник, 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. Оформили в виде плагина, см. здесь и здесь. Последнее замечание относительно неуниверсальности трансляции маппингов в новом плагине неактуально.

четверг, 13 декабря 2012 г.

Создание качественных PDF / ODP / PPT презентаций в latex

Для чего мне понадобилась корректная подсветка исходного кода Tex / minted в vim, о способе достижения которой я рассказывал здесь? Ну, например, для того, чтобы, как на то намекает название статьи, с особым комфортом и шиком генерировать качественные технические презентации прямо из vim! Например, вот такую:
Исходный код этой презентации на tex:
\documentclass{beamer}

\usepackage{lmodern}
\usepackage{minted}

\usetheme{CambridgeUS}
\usecolortheme{seahorse}

\definecolor{scriptbg}{rgb}{0.95,0.95,0.95}

\logo{\includegraphics[height=0.5cm]{MyCompanyLogo.png}}
\title {My Cool Presentation}
\author{IT Team}
\date{December 12, 2012}

\begin{document}
\maketitle

\begin{frame}
\frametitle{Our network}
\framesubtitle{(autogenerated from a dia file)}
\begin{center}
\includegraphics[width=0.8\paperwidth]{my_dia.mps}
\end{center}
\end{frame}

\begin{frame}[fragile]
\frametitle{Code samples from different languages}
\framesubtitle{(highlighted by Python Pygments via minted)}
\textbf{C++}
\begin{minted}[fontsize=\tiny,bgcolor=scriptbg,gobble=2]{c++}
  #include <iostream>
  int main( void )
  {
      std::cout << "Hello world" << std::endl;
      return 0;
  }
\end{minted}

\vspace{0.5cm}
\textbf{Python}
\begin{minted}[fontsize=\tiny,bgcolor=scriptbg,gobble=2]{python}
  #!/usr/bin/python
  print "Hello, World!"
\end{minted}

\vspace{0.5cm}
\textbf{Bash}
\begin{minted}[fontsize=\tiny,bgcolor=scriptbg,gobble=2]{sh}
  #!/usr/bin/bash
  echo Hello, World!
\end{minted}
\end{frame}

\end{document}
Кроме текста для построения презентации были использованы изображение-логотип компании MyCompanyLogo.png (надпись My Company Logo c красным кругом в правом нижнем углу каждого слайда) и файл в формате dia, из которого было автоматически сгенерировано векторное изображение my_dia.mps на втором слайде.

Кто же в ответе за всю эту красоту? Конечно же пакет beamer из репозитория tex: именно он создает структуру презентации и раскрашивает слайды в соответствии с темами, заданными командами \usetheme и \usecolortheme. Кстати, стандартные темы можно просмотреть на сайте Beamer Theme Matrix (но будьте осторожны - грузится он долго).

Пакет beamer создает прекрасные качественные презентации в формате PDF с перекрестными ссылками и панелью управления, расположенной внизу каждого слайда. А что делать, если нам нужен формат презентаций ODP OpenOffice / LibreOffice? Для этого нам нужно найти какой-нибудь качественный конвертор из PDF в ODP. В качестве движка конвертора прекрасно подходит программа pdftocairo из пакета Poppler (в моей Fedora 17 она входит в пакет rpm poppler-utils). Программа конвертора должна качественно, быстро и прозрачно преобразовать исходную презентацию в формате PDF в отдельные файлы PNG, а затем скомпоновать их в презентацию ODP.

На роль подобного менеджера подходит скрипт pdf2odp из пакета latexslides, однако он использует в качестве движка не Poppler, а Ghostscript, поэтому делает это, на мой взгляд, медленно и некачественно, кроме того, в нем нельзя задать желаемое разрешение PNG, которое всегда равно 300. Поэтому я написал патч для pdf2odp относительно текущей версии в репозитории, в котором реализованы опции по выбору движка конвертора (Ghostscript или pdftocairo из Poppler) и выходного разрешения картинок PNG. Вот этот патч:
--- bin/pdf2odp 2012-12-13 00:25:08.072750679 +0400
+++ bin/pdf2odp.new 2012-12-13 00:22:48.384713551 +0400
@@ -1,15 +1,48 @@
-#!/usr/bin/env python
+#!/usr/bin/python
+
+import sys, subprocess, os, glob, getopt
+
+def usage():
+    usage = """
+    Usage: %s [-x|--engine=] [-s|--scale=] pdffile [outfile]
+      -h --help        Prints help
+      -x --engine      Converter engine (gs or pdftocairo), default gs
+      -s --scale       Scale value, default 300
+    """
+    print usage %(os.path.basename(sys.argv[0]))
+
+# converter engine: gs or pdftocairo
+engine = 'gs'
+scale = 300
+pdffile = ''
+outfile = ''
+
+options, remainder = getopt.getopt(sys.argv[1:], 'hx:s:',
+                                   ['help','engine=', 'scale='])
+
+for opt, arg in options:
+    if opt in ('-x', '--engine'):
+        engine = arg
+    elif opt in ('-s', '--scale'):
+        scale = arg
+    elif opt in ('-h', '--help'):
+        usage()
+        sys.exit()
+
+if len(remainder) > 0:
+    pdffile = remainder[0]
+if len(remainder) > 1:
+    outfile = remainder[1]
 
-import sys, subprocess, os, glob
 # Check for odfpy and file argument
 try:
     from odf.opendocument import OpenDocumentPresentation
-    filename = sys.argv[1]
+    filename = pdffile
 except ImportError:
     print "You need odfpy, exiting."
     sys.exit(1)
 except IndexError:
-    print "Usage: %s pdfile [outfile]" %sys.argv[0]
+    usage()
     sys.exit(2)
 
 from odf.style import Style, MasterPage, PageLayout, PageLayoutProperties, \
@@ -27,21 +60,32 @@
     print "%s only accepts pdf files, exiting." %sys.argv[0]
     sys.exit(4)
 
-# Check for gs
+# Check for converter engine
 try:
-    subprocess.call(['gs', '-v'], stdout=subprocess.PIPE)
+    if engine == 'pdftocairo':
+        subprocess.call(['pdftocairo', '-v'], stdout=subprocess.PIPE)
+    else:
+        subprocess.call(['gs', '-v'], stdout=subprocess.PIPE)
 except OSError:
-    print "You need Ghostscript, exiting."
+    if engine == 'pdftocairo':
+        print "You need Poppler utils, exiting."
+    else:
+        print "You need Ghostscript, exiting."
     sys.exit(5)
 
-gs_args = ['gs', '-dNOPAUSE', '-dSAFER', '-dBATCH', '-sDEVICE=pngalpha',
-           '-r300', '-sOutputFile=tmp_%s_%%03d.png' %(file), filename]
+if engine == 'pdftocairo':
+    engine_args = ['pdftocairo', '-png', '-scale-to', '%s' %(scale), filename,
+                   'tmp_%s_' %(file)]
+else:
+    engine_args = ['gs', '-dNOPAUSE', '-dSAFER', '-dBATCH',
+                   '-sDEVICE=pngalpha', '-r%s' %(scale),
+                   '-sOutputFile=tmp_%s_%%03d.png' %(file), filename]
            
-# Try to run gs
-print 'Converting %s to images using gs\n' %filename
-result = subprocess.Popen(gs_args)
+# Try to run converter engine
+print 'Converting %s to images using %s\n' %(filename, engine)
+result = subprocess.Popen(engine_args)
 if result.wait():
-    print '\nRunning gs failed with the error above, exiting.'
+    print '\nRunning %s failed with the error above, exiting.' %engine
     sys.exit(6)
 
 print "\nDone..."
@@ -90,7 +134,7 @@
     imageframe.addElement(Image(href=href))
 
 # Save file
-file = os.path.splitext(sys.argv[2])[0] if len(sys.argv) > 2 else file
+file = os.path.splitext(outfile)[0] if len(outfile) > 0 else file
 doc.save(file, True)
 print "Presentation saved as %s.odp" %file
 
Для преобразования картинок PNG в ODP pdf2odp использует пакет odfpy, так что его тоже необходимо установить.

Преобразовать презентацию из ODP в PPT нам поможет OpenOffice или LibreOffice. И у того и у другого есть пакетный режим конвертации, который, как это ни странно, не работает, если запущен хотя бы один графический инстанс офисного приложения (sic!), поэтому команда make ppt, о которой речь пойдет ниже, не сделает ничего и завершится при этом без ошибки, если у вас открыто какое-либо офисное приложение из указанных пакетов!

Итак, речь зашла о make. Извольте, это Makefile, который делает все:
# Produce main.pdf in output directory specified in latexmkrc

GREP                =   grep
SED                 =   sed
DIA                 =   dia
LATEXMK             =   latexmk
MPOST               =   mpost
PDF2ODP             =   pdf2odp
OFFICE              =   libreoffice

LATEXMKRC           =   ./latexmkrc
PDF_MODE_PTN        =   ^\s*$$pdf_mode\s*=\s*
OUT_EXT             =   $(shell case \
                        `$(GREP) '$(PDF_MODE_PTN)' $(LATEXMKRC) 2>/dev/null | \
                        $(SED) 's/$(PDF_MODE_PTN)\([0-3]\).*/\1/'` \
                        in ([1-3]) echo pdf ;; (*) echo dvi ;; esac)
OUT_DIR_PTN         =   ^\s*$$out_dir\s*=\s*
OUT_DIR             =   $(shell \
                        $(GREP) '$(OUT_DIR_PTN)' $(LATEXMKRC) 2>/dev/null | \
                        $(SED) 's/$(OUT_DIR_PTN)["\x27]\(.*\)["\x27].*/\1/')

ifeq ($(strip $(OUT_DIR)),)
    OUT_DIR         =   .
endif

MAIN                =   main
TARGET              =   $(OUT_DIR)/$(MAIN).$(OUT_EXT)
ODP                 =   $(OUT_DIR)/$(MAIN).odp
PPT                 =   $(OUT_DIR)/$(MAIN).ppt

TEX_SOURCES         =   $(wildcard *.tex)
DIA_SOURCES         =   $(wildcard *.dia)
EPS_IMAGES          =   $(wildcard *.eps)
DIA_MP_SOURCES      =   $(DIA_SOURCES:.dia=.mp)
DIA_MPS_IMAGES      =   $(DIA_SOURCES:.dia=.mps)

DIA_MP_LOGS         =   $(DIA_SOURCES:.dia=.log)
DIA_MPX_FILES       =   $(DIA_SOURCES:.dia=.mpx)
DIA_MP_TRANS_FILES  =   $(DIA_MP_LOGS) $(DIA_MPX_FILES)
DIA_INTERMEDIATES   =   $(DIA_MP_SOURCES) $(DIA_MP_TRANS_FILES)
DIA_ALL_PRODUCTS    =   $(DIA_INTERMEDIATES) $(DIA_MPS_IMAGES)

MAIN_BBL            =   $(OUT_DIR)/$(MAIN).bbl


.PHONY: all clean clean-all odp ppt

.SECONDARY: $(DIA_MP_SOURCES)

all: $(TARGET)

odp: $(ODP)

ppt: $(PPT)

%.mp: %.dia
    $(DIA) -e $@ $<

%.mps: %.mp
    $(MPOST) -s 'outputtemplate="%j.mps"' $<

$(TARGET): $(EPS_IMAGES) $(DIA_MPS_IMAGES) $(TEX_SOURCES)
    $(LATEXMK) $(MAIN)

$(ODP): $(MAIN).pdf
    $(PDF2ODP) -x pdftocairo -s 1600 $(MAIN).pdf

$(PPT): $(ODP)
    $(OFFICE) --headless --convert-to ppt --outdir $(OUT_DIR) $(ODP)

clean:
    $(LATEXMK) -c
    rm -f $(DIA_INTERMEDIATES)

clean-all:
    $(LATEXMK) -C
    rm -f *-eps-converted-to.pdf $(DIA_ALL_PRODUCTS) $(MAIN_BBL) $(ODP) $(PPT)
К нему прилагается файл latexmkrc (он должен находится в той же директории, где находится Makefile, т.е. в нашей рабочей директории), который необходим для правильной работы latexmk:
$pdf_mode = 1;                                  # use pdflatex
$pdflatex = 'pdflatex --shell-escape %O %S'     # needed by minted
Команда make без параметров строит презентацию в формате PDF, make odp - презентацию в формате ODP, а make ppt - презентацию в формате PPT.

В данном Makefile определены абстрактные правила преобразования форматов, поэтому его можно использовать в разных проектах, связанных с tex. Главная переменная, которую, как предполагается, должен определять пользователь, это MAIN - она определяет имена исходного файла tex и сгнерированных файлов презентаций. В нашем примере предполагается, что исходный файл tex называется main.tex и, соответственно, сгенерированные файлы презентаций будут иметь имена main.pdf, main.odp и main.ppt.