Корректор раскладок «хswitcher» для linux: шаг второй

Так как предыдущая публикация (хswitcher на стадии «proof of concept») получила достаточно много конструктивных отзывов (что приятно), я продолжил тратить своё свободное время на развитие проекта. Теперь хочу потратить немножко вашего… Второй шаг будет не совсем привычный: предложение/обсуждение дизайна конфигурации.
lditri2811ilg7kt2fxabhlvxzs.jpeg
Как-то оно так получается, что нормальным программистам настраивать все эти крутилки дико скучно.

Чтобы не быть голословным, внутри пример из того с чем имею дело.

Отлично в целом задуманные (и неплохо реализованные) Apache Kafka & ZooKeeper.
— Конфигурация? Но это же скучно! Тяп-ляп xml (потому что «из коробки»).
— Ой, а вы ещё и ACL хотите? Но это так муторно! Тап-ляп… Как-то так.


А в моей работе — ровно наоборот. Правильно (увы, с первого раза почти никогда нет) построенная модель позволяет дальше легко и непринуждённо (ну, почти) собрать схему.

Попадалась недавно на Хабре статья про нелёгкую работу data-scientist’ов…

Оказывается, этот момент у них реализуется в полной мере. А в моей практике, как говорится, «версия лайт». Многотомные модели, матёрые программисты с ООП наперевес и т.д. — это всё потом появится, когда/если взлетит. А конструктору надо вот здесь и сейчас с чего-то начинать.


Ближе к делу. В качестве синтаксической основы я взял TOML вот от этого гражданина.
Потому что он (TOML) с одной стороны человеко-редактируемый. А с другой — транслируется 1:1 в любой из более распространённых синтаксисов: XML, JSON, YAML.
Более того, использованная мной реализация от «github.com/BurntSushi/toml» хотя и не самая модная (до сих пор синтаксис 1.4), зато синтаксически совместима с тем же («встроенным») JSON.
То есть, при желании можно просто сказать «иди лесом с этим твоим TOML, я хочу XXX» и «пропатчить» код всего одной строкой.
Таким образом, при желании написать для настройки хswitcher какие-то окошки (точно не я) проблем «с этим вашим долбаным конфигом» — не предвидится.
Для всех прочих синтаксис на основе «ключ = значение» (и буквально парой опций посложнее, типа = [какого, то, массива]) полагаю

интуитивно удобным.
Что любопытно, у самого «подгорело» примерно в то же время (в районе 2013 года). Только, в отличие от меня, автор TOML зашёл с должным размахом.
Поэтому сейчас мне проще подстроить его реализацию под себя, а не наоборот.


В общем, берём TOML (очень похожий на старый виндовый INI). И стоим конфигурацию, в которой описываем как прицепить серию хуков в зависимости от набора последних скен-кодов с клавиатуры. Ниже по кускам — то что на данный момент получилось. И пояснения чего это я так решил.

0. Базовые абстракции


  • Обозначения скен-кодов. С этим обязательно надо что-то делать, так как просто цифровые коды абсолютно не человеко-читаемы (это я в огород loloswitcher).
    Вытряс «ecodes.go» из «golang-evdev» (в первоисточник лезть поленился, хотя у автора он вполне культурно указан). Немножко (пока) поправил совсем уж страхолюдное. Типа «LEFTBRACE» → «L_BRACE».
  • Дополнительно ввёл понятие «клавиш с состоянием». Так как использованная регулярная грамматика не располагает к длинным пассажам. (Зато позволяет проверять с минимальными накладками. Если использовать только «прямую» запись.)
  • Будет встроенный «дедупликатор» нажатого. Таким образом, состояние «повтор»=2 будет записано один раз.


1. Раздел шаблонов

[Templates] # "@name@" to simplify expressions
 # Words can consist of these chars (regex)
 "WORD" = "([0-9A-Z`;']|[LR]_BRACE|COMMA|DOT|SLASH|KP[0-9])"


Из чего состоит слово человеческого языка с фонетической записью (то ли дело графемы aka «иероглифы»)? Какая-то ужасная «простыня». Поэтому сразу закладываю понятие «шаблона».

2. Что делать, когда что-то нажали (пришёл очередной скен-код)

[ActionKeys]
 # Collect key and do the test for command sequence
 # !!! Repeat codes (code=2) must be collected once per key!
 Add = ["1..0", "=", "BS", "Q..]", "L_CTRL..CAPS", "N_LOCK", "S_LOCK",
        "KP7..KPDOT", "R_CTRL", "KPSLASH", "R_ALT", "KPEQUAL..PAUSE",
        "KPCOMMA", "L_META..COMPOSE", "KPLEFTPAREN", "KPRIGHTPAREN"]

 # Drop all collected keys, including this.  This is default action.
 Drop = ["ESC", "-", "TAB", "ENTER", "KPENTER", "LINEFEED..POWER"]
 # Store extra map for these keys, when any is in "down" state.
 # State is checked via "OFF:"|"ON:" conditions in action.
 # (Also, state of these keys must persist between buffer drops.)
 # ??? How to deal with CAPS and "LOCK"-keys ???
 StateKeys = ["L_CTRL", "L_SHIFT", "L_ALT", "L_META", "CAPS", "N_LOCK", "S_LOCK",
              "R_CTRL", "R_SHIFT", "R_ALT", "R_META"]

 # Test only, but don't collect.
 # E.g., I use F12 instead of BREAK on dumb laptops whith shitty keyboards (new ThinkPads)
 Test = ["F1..F10", "ZENKAKUHANKAKU", "102ND", "F11", "F12",
          "RO..KPJPCOMMA", "SYSRQ", "SCALE", "HANGEUL..YEN",
          "STOP..SCROLLDOWN", "NEW..MAX"]


Всего предусмотрено 768 кодов. (Но «на всякий случай» вставил в код хswitcher отлов «сюрпризов»).
Внутри расписал заполнение массива ссылками на функции «что делать». На golang это (внезапно) оказалось удобно и очевидно.

  • «Drop» в этом месте планирую сократить до минимума. В пользу более гибкой обработки (покажу ниже).


3. Табличка с классами окон

# Some behaviour can depend on application currently doing the input.
[[WindowClasses]]
 # VNC, VirtualBox, qemu etc. emulates there input independently, so never intercept.
 # With the exception of some stupid VNC clients, which does high-level (layout-based) keyboard input.
 Regex = "^VirtualBox"
 Actions = "" # Do nothing while focus stays in VirtualBox

[[WindowClasses]]
 Regex = "^konsole"
 # In general, mouse clicks leads to unpredictable (at the low-level where xswitcher resides) cursor jumps.
 # So, it's good choise to drop all buffers after click.
 # But some windows, e.g. terminals, can stay out of this problem.
 MouseClickDrops = 0
 Actions = "Actions"

[[WindowClasses]] # Default behaviour: no Regex (or wildcard like ".")
 MouseClickDrops = 1
 Actions = "Actions"


Строки таблицы — в двойных квадратных скобках с её названием. Проще с ходу не получилось. В зависимости от текущего активного окна, можно подобрать опции:

  • Свой набор «горячих клавиш» «Actions = …». Если нет/пусто — ничего не делать.
  • Переключатель «MouseClickDrops» — что делать при обнаружении клика мышкой. Так как в точке включения xswitcher нет никаких подробностей «куда там щёлкают», по-умолчанию сбрасываем буфер. Но в терминалах (например) можно так и не делать (как правило).


4. Одна (или несколько) последовательностей нажатий запускают тот или иной хук

# action = [ regex1, regex2, ... ]
# "CLEAN" state: all keys are released
[Actions]
# Inverse regex is hard to understand, so extract negation to external condition.
# Expresions will be checked in direct order, one-by-one. Condition succceds when ALL results are True.
 # Maximum key sequence length, extra keys will be dropped. More length - more CPU.
 SeqLength = 8
 # Drop word buffer and start collecting new one
 NewWord = [ "OFF:(CTRL|ALT|META)  SEQ:(((BACK)?SPACE|[LR]_SHIFT):[01],)*(@WORD@:1)", # "@WORD@:0" then collects the char
             "SEQ:(@WORD@:2,@WORD@:0)", # Drop repeated char at all: unlikely it needs correction
             "SEQ:((KP)?MINUS|(KP)?ENTER|ESC|TAB)" ] # Be more flexible: chars line "-" can start new word, but must not completelly invalidate buffer!
 # Drop all buffers
 NewSentence = [ "SEQ:(ENTER:0)" ]

 # Single char must be deleted by single BS, so there is need in compose sequence detector.
 Compose = [ "OFF:(CTRL|L_ALT|META|SHIFT)  SEQ:(R_ALT:1,(R_ALT:2,)?(,@WORD@:1,@WORD@:0){2},R_ALT:0)" ]

 "Action.RetypeWord" = [ "OFF:(CTRL|ALT|META|SHIFT)  SEQ:(PAUSE:0)" ]
 "Action.CyclicSwitch" = [ "OFF:(R_CTRL|ALT|META|SHIFT)  SEQ:(L_CTRL:1,L_CTRL:0)" ] # Single short LEFT CONTROL
 "Action.Respawn" = [ "OFF:(CTRL|ALT|META|SHIFT)  SEQ:(S_LOCK:2,S_LOCK:0)" ] # Long-pressed SCROLL LOCK

 "Action.Layout0" = [ "OFF:(CTRL|ALT|META|R_SHIFT)  SEQ:(L_SHIFT:1,L_SHIFT:0)" ] # Single short LEFT SHIFT
 "Action.Layout1" = [ "OFF:(CTRL|ALT|META|L_SHIFT)  SEQ:(R_SHIFT:1,R_SHIFT:0)" ] # Single short RIGHT SHIFT

 "Action.Hook1" = [ "OFF:(CTRL|R_ALT|META|SHIFT)  SEQ:(L_ALT:1,L_ALT:0)" ]


Хуки разделил на два типа. Встроенные, с «говорящими» именами (NewWord, NewSentence, Compose) и программируемые.
Названия программируемых начинаются с «Action.». Т.к. TOML v1.4, имена с точками должны быть в кавычках.
Ниже для каждого должен быть описан раздел с таким же названием.

Чтобы не взрывать людям мозг «голыми» регулярками (по опыту, их написать-то может один из десяти профессионалов), сразу внедряю дополнительный синтаксис.

  • «OFF:» (или «ON:») перед regexp (регулярным выражением) требуют чтобы указанные далее кнопки были отпущены (или нажаты).
    Дальше собираюсь сделать «нечестное» регулярное выражение. С раздельной проверкой кусков между пайпами »|». С целью уменьшения количества записей вида »[LR]_SHIFT» (там где это явно не надо).
  • «SEQ:» Если предыдущее условие выполнено (или отсутствует), дальше проверяем относительно «обычного» регулярного выражения. За подробностями сразу посылаю на^Wв библиотеку «regexp». Потому что сам до сих пор не удосужился выяснить степень совместимости с моими любимыми pcre («perl compatible»).
  • Выражение записывается в виде «КНОПКА_1: КОД1, КНОПКА_2: КОД2» и т.д., в порядке поступления скен-кодов.
  • Проверка всегда «прижимается» к концу последовательности, поэтому »$» в хвост дописывать не надо.
  • Все проверки в одной строке выполняются друг за другом и объединяются по «И». Но так как значение описано в виде массива, можно после запятой написать альтернативную проверку. Если это зачем-то нужно.
  • Значение «SeqLength = 8» ограничивает размер буфера, относительно которого выполняются все проверки. Т.к. в жизни мне (до сих пор) не встречались бесконечные ресурсы.


5. Задание хуков, расписанных в предыдущей секции

# Action is the array, so actions could be chained (m.b., infinitely... Have I to check this?).
# For each action type, extra named parameters could be collected. Invalid parameters will be ignored(?).
[Action.RetypeWord] # Switch layout, drop last word and type it again
 Action = [ "Action.CyclicSwitch", "RetypeWord" ] # Call Switch() between layouts tuned below, then RetypeWord()

[Action.CyclicSwitch] # Cyclic layout switching
 Action = [ "Switch" ] # Internal layout switcher func
 Layouts = [0, 1]

[Action.Layout0] # Direct layout selection
 Action = [ "Layout" ] # Internal layout selection func
 Layout = 0

[Action.Layout1] # Direct layout selection
 Action = [ "Layout" ] # Internal layout selection func
 Layout = 1

[Action.Respawn] # Completely respawn xswitcher. Reload config as well
 Action = [ "Respawn" ]

[Action.Hook1] # Run external commands
  Action = [ "Exec" ]
  Exec = "/path/to/exec -a -b --key_x"
  Wait = 1
  SendBuffer = "Word" # External hook can process collected buffer by it's own means.


Основное тут — «Action = [Массив]». Аналогично предыдущей секции, есть ограниченный набор встроенных действий. И не ограниченная в принципе возможность стыковки (написать «Action.XXX» и не полениться расписать под него ещё одну секцию).
В том числе, перенабор слова в исправленной раскладке разделяется на две части: «поменяй раскладку как вон там задано» и «перенабери» («RetypeWord»).
Остальные параметры записываются в «словарь» («map» в golang) для данного действия, их список зависит от написанного в «Action».
Несколько разных действий можно описать в одной куче (секции). А можно растащить. Как я выше показал.
Сразу закладываю действие «Exec» — выполнить внешний сценарий. С опцией затолкать ему в stdin записанный буфер.

  • «Wait = 1» — подождать завершения запущенного процесса.
  • Вероятно, «до кучи» захочется выставлять в окружение доп. информацию типа имени класса окна из которого перехвачено.
    «Хотите подключить свой обработчик? Вам вот сюда.»

Уф (выдохнул). Вроде ничего не забыл.

Оп! Ага, не забыл…
А конфигурация запуска где? В хард-коде́? Примерно так:
[ScanDevices]
 # Must exist on start. Self-respawn in case it is younger then 30s
 Test = "/dev/input/event0"
 Respawn = 30
 # Search mask
 Search = "/dev/input/event*"
 # In my thinkPads there are such a pseudo-keyboards whith tons of unnecessary events
 Bypass = "(?i)Video|Camera" # "(?i)" obviously differs from "classic" pcre's.


А где забыл/ошибся (без этого — никак), очень надеюсь что внимательные читатели не поленятся ткнуть носом.
Удачи!

© Habrahabr.ru