вторник, 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.

среда, 12 декабря 2012 г.

vim и tex: подсветка внешнего кода minted

Текущая ситуация с подсветкой кода minted в исходниках tex в vim оставляет желать лучшего. Я не стану с уверенностью утверждать, что она так же плоха в различных плагинах tex в vim: я ими не пользуюсь по причине излишней тяжеловесности. Но стандартный синтаксический файл tex.vim не умеет работать с подсветкой minted. Вот вам пример:
\documentclass{article}

\usepackage{minted}

\begin{document}

Here is a \textbf{C++} Hello World example:

\begin{minted}[fontsize=\tiny,linenos=false,gobble=2]{c++}
  #include <iostream>
  int main( void )
  {
      std::cout << "Hello world" << std::endl;
      return 0;
  }
\end{minted}

Here is a \textbf{Python} Hello World example:

\begin{minted}[fontsize=\tiny,linenos=false,gobble=2]{python}
  #!/usr/bin/python
  print "Hello, World!"
\end{minted}

Here is a \textbf{Unknown} Hello World example:

\begin{minted}[fontsize=\tiny,linenos=false,gobble=2]{unknown}
  $my_directive <- { p : 10 }{ p : 20 }
  end $my_directive
\end{minted}

Bye.

\end{document}

Видите, гипотетический язык Unknown уже подсвечивается странно, а сейчас мы вставим киллер-код на sh, который полностью уничтожит синтаксис tex:
\documentclass{article}

\usepackage{minted}

\begin{document}

Here is a highlight killer:

\begin{minted}[fontsize=\tiny,linenos=false,gobble=2]{sh}
  #!/usr/bin/sh
  a=$HOME; hello_world="Hello world"
\end{minted}

Here is a \textbf{C++} Hello World example:

\begin{minted}[fontsize=\tiny,linenos=false,gobble=2]{c++}
  #include <iostream>
  int main( void )
  {
      std::cout << "Hello world" << std::endl;
      return 0;
  }
\end{minted}

Here is a \textbf{Python} Hello World example:

\begin{minted}[fontsize=\tiny,linenos=false,gobble=2]{python}
  #!/usr/bin/python
  print "Hello, World!"
\end{minted}

Here is a \textbf{Unknown} Hello World example:

\begin{minted}[fontsize=\tiny,linenos=false,gobble=2]{unknown}
  $my_directive <- { p : 10 }{ p : 20 }
  end $my_directive
\end{minted}

Bye.

\end{document}

Это катастрофа! Я хочу, чтобы это выглядело так:
\documentclass{article}

\usepackage{minted}

\begin{document}

Here is a highlight killer:

\begin{minted}[fontsize=\tiny,linenos=false,gobble=2]{sh}
  #!/usr/bin/sh
  a=$HOME; hello_world="Hello world"
\end{minted}

Here is a \textbf{C++} Hello World example:

\begin{minted}[fontsize=\tiny,linenos=false,gobble=2]{c++}
  #include <iostream>
  int main( void )
  {
      std::cout << "Hello world" << std::endl;
      return 0;
  }
\end{minted}

Here is a \textbf{Python} Hello World example:

\begin{minted}[fontsize=\tiny,linenos=false,gobble=2]{python}
  #!/usr/bin/python
  print "Hello, World!"
\end{minted}

Here is a \textbf{Unknown} Hello World example:

\begin{minted}[fontsize=\tiny,linenos=false,gobble=2]{unknown}
  $my_directive <- { p : 10 }; { p : 20 }
  end $my_directive
\end{minted}

Bye.

\end{document}

Участок с sh подсвечен синтаксисом sh, с C++ - синтаксисом C++, с Python - синтаксисом Python, а нечто непонятное Unknown - синтаксисом texZone из файла syntax/tex.vim. Язык программирования - параметр minted в фигурных скобках - подсвечен отдельно, в данном случае цветом цветовой группы Special.

Теперь о том, как этого добиться. Поскольку плагин Vimwiki уже умеет загружать синтаксическую подсветку для разных языков программирования (об этой возможности Vimwiki я рассказывал здесь), то пусть он потрудится и для tex. Я не шучу! Мы напишем наш after-syntax файл $HOME/.vim/after/syntax/tex.vim в котором будет использован вызов VimwikiGet() и слегка адаптированная функция vimwiki#base#nested_syntax()! Вот его содержание, а комментарии ниже:
" content highlights inside lstlisting must be disabled
syntax region texZone start="\\begin{lstlisting}" end="\\end{lstlisting}"
" fallback option for minted is also texZone
syntax region texZone start="\\begin{minted}" end="\\end{minted}"

if !exists('g:tex_hl_minted') || g:tex_hl_minted == 0
    finish
endif

" Vimwiki's function vimwiki#base#nested_syntax() adaptation
functions:nested_syntax(hltype, filetype, start, end) abort
" From http://vim.wikia.com/wiki/VimTip857
  let ft=toupper(a:filetype)
  let group='textGroup'.ft
  if exists('b:current_syntax')
    let s:current_syntax=b:current_syntax
    " Remove current syntax definition, as some syntax files (e.g. cpp.vim)
    " do nothing if b:current_syntax is defined.
    unlet b:current_syntax
  endif

  " Some syntax files set up iskeyword which might scratch vimwiki a bit.
  " Let us save and restore it later.
  " let b:skip_set_iskeyword = 1
  let is_keyword = &iskeyword

  try
    " keep going even if syntax file is not found
    execute 'syntax include @'.group.' syntax/'.a:filetype.'.vim'
    execute 'syntax include @'.group.' after/syntax/'.a:filetype.'.vim'
  catch
  endtry

  execute 'syntax match texExtCodeLang "{\@<='.a:hltype.'\ze}"'
  execute 'syntax region texMintedStart start="\\begin{minted}" end="}" '.
              \ 'contains=texSectionMarker,texBeginEnd,texExtCodeLang '.
              \ 'nextgroup=@'.group.' contained'

  let &iskeyword = is_keyword

  if exists('s:current_syntax')
    let b:current_syntax=s:current_syntax
  else
    unlet b:current_syntax
  endif

  execute 'syntax region textSnip'.ft.
        \ ' start="'.a:start.'" end="'.a:end.'"'.
        \ ' contains=@'.group.',texMintedStart '.
        \ 'containedin=texDocZone,texPartZone,texChapterZone,'.
        \ 'texSectionZone,texSubSectionZone,texSubSubSectionZone,'.
        \ 'texParaZone,texSubParaZone,texAbstract keepend'

  " A workaround to Issue 115: Nested Perl syntax highlighting differs from
  " regular one.
  " Perl syntax file has perlFunctionName which is usually has no effect due to
  " 'contained' flag. Now we have 'syntax include' that makes all the groups
  " included as 'contained' into specific group.
  " Here perlFunctionName (with quite an angry regexp "\h\w*[^:]") clashes with
  " the rest syntax rules as now it has effect being really 'contained'.
  " Clear it!
  if ft =~ 'perl'
    syntax clear perlFunctionName
  endif
endfunction

let s:regStart = '\\begin{minted}\s*\n*\(\[\_[^]]*\]\)*\s*\n*{'
let s:regEnd ='\ze\\end{minted}'

let s:nested = VimwikiGet('nested_syntaxes')

if !empty(s:nested)
  for [s:hl_syntax, s:vim_syntax] in items(s:nested)
    if s:vim_syntax == 'tex'
      continue
    endif
    call s:nested_syntax(s:hl_syntax, s:vim_syntax,
                \ s:regStart.s:hl_syntax.'}', s:regEnd)
  endfor
endif

hi link texExtCodeLang Special

Чтобы это правильно работало, не забудьте вставить в .vimrc определение g:WikiGlobal.nested_syntaxes с указанием тех языков программирования, которые вы намерены подсвечивать в Vimwiki и Tex / minted. Кроме того, добавьте туда строки
let g:tex_isk = '48-57,a-z,A-Z,192-255,_'
let g:tex_hl_minted = 1
Теперь комментарии по коду. Первые два определения syntax region указывают, что мы хотим подсвечивать области, начинающиеся с \begin{lstlisting} или \begin{minted} и заканчивающиеся на \end{lstlisting} или \end{minted} соответственно, одним цветом региона texZone. Директива lstlisting - это часть пакета tex listings, который похож на minted, но, в отличие от последнего, использует не Python Pygments, а какие-то собственные алгоритмы. Его было бы тоже неплохо подсвечивать синтаксисом используемого языка программирования, но, к сожалению, в listings , в отличие от minted, язык задается в отдельной директиве.

Почему мы здесь задаем одноцветную подсветку texZone и для minted тоже? Ведь мы собирались использовать полноценный синтаксис языка программирования внутри его региона! Ответ прост - это fallback режим для языков типа Unknown, не указанных в g:WikiGlobal.nested_syntaxes. Более точные определения синтаксических регионов для всех сконфигурированных в g:WikiGlobal.nested_syntaxes языков задаются в цикле for в нижней части приведенного кода. Переменная s:nested, являющаяся выходным значением функции VimwikiGet(), содержит маппинг, заданный в переменной g:WikiGlobal.nested_syntaxes.

Главная задача цикла for - настройка уточненных синтаксических регионов minted для всех сконфигурированных языков программирования (кроме собственно tex - иначе мы получим одноцветный регион texZone для всех языков). В цикле происходит вызов функции s:nested_syntax() - слегка видоизмененной vimwiki#base#nested_syntax(). Изменений там немного. Во-первых изменен прототип: первым аргументом добавлен ключ из элемента  g:WikiGlobal.nested_syntaxes - он нужен для формирования синтаксической области texExtCodeLang - см. далее, и убран последний аргумент textSnipHl, который соответствовал ненужному нам matchgroup в синтаксическом регионе textSnip<Lang> (где <Lang> - синтаксическая группа, соответствующая конфигурируемому языку). Добавлены определения syntax match texExtCodeLang и syntax region texMintedStart, который внесен в список contains в определении syntax region textSnip<Lang>. Определение синтаксического региона textSnip - главная задача функции s:nested_syntax().

Синтаксический регион texMintedStart соответствует началу области minted и соответствует сложному регулярнуму выражению, заданному в переменной s:regStart. Сложность выражения позволяет разбивать отдельные элементы преамбулы \begin{minted} на отдельные строки. Область texExtCodeLang соответствует спецификации языка программирования в преамбуле minted и будет подсвечиваться цветом группы Special (см. последнюю строку приведенного кода).

Ну вот собственно и все, теперь все подсвечивается правильно. Спасибо Vimwiki!

Напоследок хочу уточнить, что указание переменной g:tex_isk будет работать только в последних версиях syntax/tex.vim. Например, в стандартном пакете vim в Fedora 17 используется старая версия этого файла, поэтому я положил текущую версию из репозитория vim к себе в $HOME/.vim/syntax/. Вообще, g:vim_isk нужен нам только для задания символа подчеркивания (_) в качестве символа iskeyword для файлов tex. В нашем случае это исправит возможные искажения синтаксиса внутри регионов minted, если в них используются символы подчеркивания. Для файлов tex символ подчеркивания был убран из списка iskeyword по каким-то соображениям, однако в последних версиях syntax/tex.vim появилась возможность вернуть его с помощью переменной g:tex_isk. Если у вас старая версия syntax/tex.vim и вы не хотите устанавливать новую версию в $HOME/.vim/syntax/, то можете просто добавить строку
setlocal iskeyword+=_
в файл $HOME/.vim/after/syntax/tex.vim.

четверг, 29 ноября 2012 г.

Закрашивание полей в linux консоли - баг или фича?

Пока разбирался с Powerline в linux консоли, обнаружил весьма интересный и поучительный баг. Поучительный - потому что пришлось просмотреть исходный код сразу четырех компонентов - плагина Powerline, vim, эмулятора видеотерминала и framebuffer драйвера в ядре linux. Баг проявляется при переключении между виртуальными консолями (в т.ч. графической) и заключается в закрашивании одного из полей монитора каким-либо цветом. На этой картинке цветная полоса находится внизу. Как это воспроизвести: запустите vim в консоли, но таким образом, чтобы сразу появлялась цветная статусная строка, для этого в $HOME/.vimrc должна присутствовать строка
set laststatus=2
Затем сразу же переключитесь в другую консоль и обратно - вы получите полосу, закрашенную цветом статусной строки в одном из полей консоли. Если теперь вы закроете vim и снова переключитесь в другую консоль и обратно, то цветная полоса уйдет.

Сначала я подумал, что во всем виноваты Powerline или vim, но потом до меня дошло, что user space программа не может быть виновна в закрашивании области, которая ей даже не доступна! Значит проблема в ядре, в частности в эмуляторе видеотерминала или драйвере фреймбуфера. В подтверждение этому - следующий эксперимент.

Запустите в консоли любую псевдографическую программу, например в своей системе я нашел программу system-config-firewall-tui. Программа переведет консоль в псевдографический режим с синим полем и тремя кнопками (одна из них - Отмена, так что ничего страшного после ее запуска не не произойдет). Внимательно посмотрите на края монитора. Теперь переключитесь в другую консоль и обратно. Количество синего увеличилось, не так ли? Закройте программу и нажмите несколько раз Enter чтобы добавить черного цвета снизу. Видите точно такую же полосу, как в vim, только синего цвета? Еще одно переключение туда-обратно сотрет ее.

Изучение кода ядра вскрыло проблему. Оказывается, при переключении консоли в функции fbcon_switch() (см. drivers/video/console/fbcon.c) вызывается функция fbcon_clear_margins(), которая вызывает метод ops->clear_margins(), который, в свою очередь, закрашивает поля (margins, но как правило, это одно поле) цветом vc->vc_video_erase_char (см. функцию ud_clear_margins() в drivers/video/console/fbcon_ud.c и подобные им в других подобных файлах, которые в свою очередь вызывают функцию attr_col_ec() из drivers/video/console/fbcon.h). Проблема в том, что цвет  vc->vc_video_erase_char в псевдографике может зависеть от погоды и расположения звезд.

Зачем это сделано? Как я понял, некоторые режимы фреймбуфера могут использовать поля монитора, и при переключении в другую консоль эти поля нужно очищать, чтобы не наблюдать все время, например, кусок застывшего видео сбоку или снизу. Как очищать, каким цветом? В интернете я нашел патч, написанный еще в 2003 году, в котором предлагалось закрашивать поля черным цветом (см. здесь и здесь), однако, как я понял из дискуссии, использовать черный цвет не всегда правильно, поскольку графические дисплеи могут быть разные. Таким образом, закрашивание цветом erase char - это меньшее из зол.

воскресенье, 25 ноября 2012 г.

Шрифты Powerline в linux консоли

Отвечая на вопрос о шрифтах Powerline в linux консоли (см. комментарии к этому посту), я привел варианты, как этого можно добиться. Ни один вариант для меня не подошел в силу разных обстоятельств. Добавлять новые символы в консоли оказалось вообще не просто и какого-то единого алгоритма попросту не существует. Но мне все-таки удалось с этим справиться, и здесь я хочу поведать об этой истории успеха. Все, что здесь будет сказано, относится к Fedora 17, для других дистрибутивов действия могут слегка отличаться.


Картинка большая и кликабельная. Символы из Powerline видны в статусной строке vim (там где им и положено находиться), а также внутри присваиваемого значения переменной PROMPTLINE в функции proml редактируемого файла .bashrc (курсор находится между словами function и proml, создавая впечатление, что это одно слово, соединенное символом подчеркивания). Цветов в статусной строке нет по той простой причине, что в дефолтной схеме Powerline используется 256-цветная палитра, которая не работает в linux консоли. Так что если вы хотите нормальную цветную статусную строку Powerline в linux консоли, вам придется изменить дефолтную цветовую схему Powerline, или создать новую (схемы находятся в директории $HOME/.vim/autoload/Powerline/Colorschemes/). Также хочу отметить, что эта картинка была получена с помощью замечательной программы fbgrab из пакета fbcat, который я взял отсюда (кликнув на строке Fedora 17).

Итак, что нам понадобится для создания нового шрифта с символами из Powerline для linux консоли? Прежде всего, понимание того, что нам нужен шрифт с весьма специфичным форматом PSF, который, хотя и поддерживает Unicode mapping, все же не может содержать более 512 символов. В Fedora 17 psf шрифты находятся в директории /lib/kbd/consolefonts/. При включении режима Unicode в консоли c помощью команды unicode_start, попросту загружается новый шрифт с необходимыми символами, в частности у меня в Fedora это /lib/kbd/consolefonts/UniCyr_8x16.psf.gz, который содержит всего 256 символов. Если в вашей консоли используется шрифт terminus, то название файла будет, естественно, другим. Таблица соответствия с символами Unicode прописана внутри файла шрифта, и просмотреть ее можно с помощью команды psfgettable. Таким образом, задача сводится к созданию нового psf файла, в который добавлены символы из Powerline, при этом общее количество символов в файле не должно превышать 512. В дальнейшем, в качестве базового шрифта я буду использовать UniCyr_8x16.psf.

Что нам понадобится из программного обеспечения? Первое - программа otf2bdf для создания шрифта в промежуточном формате bdf: ее в Fedora 17 можно установить с помощью yum. Второе - программа gbdfed для конвертации шрифта bdf в формат psf: она также устанавливается с помощью yum. Третье - пакет PSF tools для манипуляции с psf шрифтами: его я скачал отсюда. Четвертое - удобный текстовый редактор (например, vim) для удаления артефактов конвертации в текстовом представлении шрифта. Пятое - исходный файл с базовыми кириллическими символами, в который мы внедрим новые символы Powerline, как я уже говорил это будет файл UniCyr_8x16.psf из директории /lib/lbd/consolefonts/.

Итак, приступим. Первое, что мы сделаем - это выделим все символы из Powerline (всего их девять) в отдельный шрифт в формате bdf. Эта задача вполне по плечу программе otf2bdf. Исходный файл должен быть получен с помощью программы  fontpatcher из плагина Powerline и иметь формат otf или ttf (см. подробности здесь). У меня данный файл называется DejaVuSansMonoForPowerline.ttf. Программа otf2bdf должна знать, какие символы нам нужны, а также соответствующие им значения Unicode. Для этого мы создадим вспомогательный файл dvusmpl.bdfmap (название не имеет значения), в котором зададим этот маппинг:
REGISTRY ISO10646
ENCODING 1
0x60 0x2B60
0x61 0x2B61
0x62 0x2B62
0x63 0x2B63
0x64 0x2B64
0x80 0x2B80
0x81 0x2B81
0x82 0x2B82
0x83 0x2B83
Строки REGISTRY и ENCODING могут содержать достаточно произвольные значения: в нашем случае они не будут важны. Далее прописан маппинг номер символа : значение Unicode. Номера символов в принципе тоже не имеют значения, однако здесь я сопоставил их со значениями Unicode, которые соответствуют символам Powerline. Первая попытка:
otf2bdf -v -c M -l '96_131' -o dvusmpl.bdf -m dvusmpl.bdfmap DejaVuSansMonoForPowerline.ttf
не увенчается успехом из-за размера сгенерированного шрифта, который окажется 12x20 вместо 8x16 (bdf формат имеет текстовое представление и размер шрифта можно найти в строке FONTBOUNDINGBOX в заголовке файла). otf2bdf позволяет настроить размер шрифта с помощью опций -rh и -rv, значения которых, однако, соответствуют не количеству пикселов по горизонтали и вертикали, а количеству точек на дюйм, поэтому поиск подходящих значений - задача творческая. После нескольких попыток я наконец подобрал подходящие значения для этих опций:
otf2bdf -v -c M -rh 66 -rv 86 -l '96_131' -o dvusmpl.bdf -m dvusmpl.bdfmap DejaVuSansMonoForPowerline.ttf
Опция -c M указывает на то, что мы хотим создать моноширинный шрифт, а опция -l '96_131' задает диапазон символов для генерации (десятиричное число 91 соответствует шестнадцатиричному 0x60, a 131 - 0x83).

Теперь у нас есть файл c символами Powerline в формате bdf. Открываем его в программе gbdfed, убеждаемся, что все 9 символов на месте (скорее всего gbdfed разобьет шрифт на 2 страницы, при этом первые 5 символов окажутся на первой, а оставшиеся 4 - на второй), и экспортируем его в формат PSF (в меню File программы). После экспорта файла у нас появится файл dvusmpl.psfu.

Следующий шаг - переводим файл dvusmpl.psfu в текстовый формат с помощью программы psf2txt из PSF Tools.
psf2txt dvusmpl.psfu dvusmpl.txt
Открываем dvusmpl.txt в текстовом редакторе и правим псевдографические изображения символов по своему вкусу: это необходимо, так как odf2bdf в процессе масштабирования шрифта исказила исходные символы (главным образом стрелки). Поскольку я все это уже проделал, то результат (с дополнительными правками - см. ниже) проще скачать отсюда.

Теперь берем исходный PSF файл UniCyr_8x16.psf.gz, раззиповываем его и переводим в текстовый формат:
psf2txt UniCyr_8x16.psf UniCyr_8x16ForPowerline.txt
В конец нового файла UniCyr_8x16ForPowerline.txt вставляем содержимое файла dvusmpl.txt, меняем  строки // Character 0 .. 8 во вставленном участке на // Character 256 .. 264. Также добавляем во вставленном участке строки с Unicode информацией для каждого отдельного символа таким же образом, как это сделано для символов исходного psf шрифта. Если вы скачали выложенный мною файл dvusmpl.txt, то достаточно просто добавить его в конец файла UniCyr_8x16ForPowerline.txt, так как все необходимые изменения в нем уже есть. Теперь идем в начало файла, ищем строку Length: 256 и заменяем ее на Length: 265 (мы ведь добавили 9 новых символов).

Переводим UniCyr_8x16ForPowerline.txt в формат psf c помощью программы txt2psf из PSF Tools:
txt2psf UniCyr_8x16ForPowerline.txt UniCyr_8x16ForPowerline.psf
Можно убедиться, что новые символы присутствуют в UniCyr_8x16ForPowerline.psf:
psfgettable PowerLineSymbols1.psf | grep -i 2b80
Зипуем файл и кладем его в директорию /lib/kbd/consolefonts/. Логинимся в виртуальной консоли и загружаем новый шрифт:
unicode_start UniCyr_8x16ForPowerline
Теперь открываем vim и смотрим, присутствуют ли в статусной строке символы из Powerline.

Если есть желание загружать новый шрифт каждый раз при входе в систему из виртуальной консоли, то можно добавить строку unicode_start UniCyr_8x16ForPowerline для linux консоли в какой-нибудь файл из директории /etc/profile.d/ (см. подробно здесь). Кроме того, теперь и в linux консоли вы сможете сделать приглашение командной строки более привлекательным (см. здесь).

Update. Выяснилось, что дефолтный консольный Unicode шрифт в Fedora 17 - это latarcyrheb-sun16 (загляните внутрь скрипта /usr/bin/unicode_start и поймете почему). В шрифте определены 512 символов, поэтому, если брать его за основу, следует не добавлять дополнительные символы, а заменить те из существующих, которые вы вряд ли будете использовать, например в позициях 460 - 488 находятся символы арабского алфавита, и если вы не читаете тексты на арабском, то можете смело вставлять новые символы куда-нибудь внутрь этого диапазона.

Последние изменения. Картинка:


Все цвета в статусной строке присутствуют, Unicode символы на месте (в т.ч. стрелочки в окне tagbar), я специально вышел из vim, чтобы показать, что в строке приглашения тоже находятся символы из Powerline.

За базовый шрифт я взял /lib/kbd/consolefonts/latarcyrheb-sun16.psfu.gz. Распаковываем, переводим в текст с помощью psf2txt, заменяем символы с 460 по 472 этим. Переводим обратно в psf:
txt2psf latarcyrheb-sun16.txt latarcyrheb-sun16_vim-unicode.psfu
Зипуем latarcyrheb-sun16_vim-unicode.psfu и кладем в /lib/kbd/consolefonts/. Файл /etc/profile.d/console_unicode.sh теперь выглядит так:
if [ "$TERM" = "linux" ]; then
    unicode_start latarcyrheb-sun16_vim-unicode
    export CONSOLEFONT_HAS_VIM_UNICODE_SYMB=1
fi
Изменения в файле $HOME/.bashrc:
case $COLORTERM in
    gnome*|mate*|konsole*)
        TERM=xterm-256color
        ;;
esac

[ -n "$XTERM_SHELL" ] && COLORTERM=xterm-256color

function proml
{
    case $COLORTERM in
        gnome*|mate*|konsole*)
            local PROMPTLINE="\[\033[38;5;167m\]\$(date +%d/%m/%y\ %H:%M)⮁⮁ \
\[\033[38;5;173m\]\u@\[\033[38;5;140m\]\h⮁⮁\[\033[38;5;173m\] \W \[\033[0m\] "
            local PROMPTLINE2='\[\033[38;5;196m\]⮀\[\033[0m\] '
            ;;
        xterm*)
            local PROMPTLINE="\[\033[38;5;167m\](\$(date +%d/%m/%y\ %H:%M))\
\[\033[38;5;173m\][\u@\[\033[38;5;140m\]\h\[\033[38;5;173m\] \W]$\[\033[0m\] "
            local PROMPTLINE2='\[\033[38;5;196m\]>\[\033[0m\] '
            ;;
        *)
            if [ "$TERM" = "linux" ] && \
               [ -n "$CONSOLEFONT_HAS_VIM_UNICODE_SYMB" ] ; then
                local PROMPTLINE="\[\033[32m\]\$(date +%d/%m/%y\ %H:%M)⮁⮁ \
\[\033[0m\]\u@\[\[\033[1;34m\]\h\[\033[0m\]⮁⮁\[ \W \033[32m\]\[\033[0m\] "
                local PROMPTLINE2='\[\033[31m\]⮀\[\033[0m\] '
            else
                local PROMPTLINE="(\$(date +%d/%m/%y\ %H:%M))[\u@\h \W]$ "
                local PROMPTLINE2=''
            fi
            ;;
    esac
    PS1=$PROMPTLINE
    PS2=$PROMPTLINE2
}

proml
unset proml
Изменения в $HOME/.vimrc:
" ---- Powerline settings
" ----
" disable Unicode symbols in linux console if font does not support them
let g:DisableUnicodeSymbols = &term =~? '^linux' &&
            \ empty($CONSOLEFONT_HAS_VIM_UNICODE_SYMB)

let g:Powerline_theme = 'default'

" solarized theme is suitable for linux console
if &t_Co < 256
    let g:Powerline_colorscheme = 'solarized'
else
    let g:Powerline_colorscheme = 'default'
endif

" fancy symbols need patched font!
if g:DisableUnicodeSymbols
    let g:Powerline_symbols = 'compatible'
else
    let g:Powerline_symbols = 'fancy'
endif

" do not use Powerline in simple console terminals
let g:DisablePowerline = &term =~? '^linux' && 0

if g:DisablePowerline
    let g:Powerline_loaded = 0
endif

if !empty($TMPDIR)
    let g:Powerline_cache_file = $TMPDIR."/Powerline_".$USER."_".
                \ g:Powerline_theme."_".g:Powerline_colorscheme."_".
                \ g:Powerline_symbols.".cache"
else
    let g:Powerline_cache_file = '/tmp/Powerline_'.$USER.'_'.
                \ g:Powerline_theme.'_'.g:Powerline_colorscheme.'_'.
                \ g:Powerline_symbols.'.cache'
endif
(я сюда еще правильную обработку кэша цветовой схемы Powerline добавил).

Да, и самое главное - патч для  цветовой схемы solarized.vim. Без него статусная строка Powerline в консоли правильно работать не будет!
--- autoload/Powerline/Colorschemes/solarized.vim   2012-11-27 13:46:28.923529118 +0400
+++ autoload/Powerline/Colorschemes/solarized.vim.new   2012-11-27 13:35:09.794349678 +0400
@@ -3,19 +3,19 @@
 " N = no focus
 " 16 hex colors as defined on http://ethanschoonover.com/solarized
 call Pl#Hi#Allocate({
-  \ 'base03'  : [8,   0x002b36],
+  \ 'base03'  : [&t_Co > 8 ? 8 : '0*',   0x002b36],
   \ 'base02'  : [0,   0x073642],
-  \ 'base01'  : [10,  0x586e75],
-  \ 'base00'  : [11,  0x657b83],
-  \ 'base0'   : [12,  0x839496],
-  \ 'base1'   : [14,  0x93a1a1],
+  \ 'base01'  : [&t_Co > 8 ? 10 : '2*',  0x586e75],
+  \ 'base00'  : [&t_Co > 8 ? 11 : '6*',  0x657b83],
+  \ 'base0'   : [&t_Co > 8 ? 12 : '1*',  0x839496],
+  \ 'base1'   : [&t_Co > 8 ? 14 : '3*',  0x93a1a1],
   \ 'base2'   : [7,   0xeee8d5],
-  \ 'base3'   : [15,  0xfdf6e3],
+  \ 'base3'   : [&t_Co > 8 ? 15 : '7*',  0xfdf6e3],
   \ 'yellow'  : [3,   0xb58900],
-  \ 'orange'  : [9,   0xcb4b16],
+  \ 'orange'  : [&t_Co > 8 ? 9 : '4*',   0xcb4b16],
   \ 'red'     : [1,   0xdc322f],
   \ 'magenta' : [5,   0xd33682],
-  \ 'violet'  : [13,  0x6c71c4],
+  \ 'violet'  : [&t_Co > 8 ? 13 : '5*',  0x6c71c4],
   \ 'blue'    : [4,   0x268bd2],
   \ 'cyan'    : [6,   0x2aa198],
   \ 'green'   : [2,   0x859900],
(зафайлил баг в Powerline, надеюсь починят).