Корректор раскладок «хswitcher» для linux: шаг второй
Так как предыдущая публикация (хswitcher на стадии «proof of concept») получила достаточно много конструктивных отзывов (что приятно), я продолжил тратить своё свободное время на развитие проекта. Теперь хочу потратить немножко вашего… Второй шаг будет не совсем привычный: предложение/обсуждение дизайна конфигурации.
Как-то оно так получается, что нормальным программистам настраивать все эти крутилки дико скучно.
Чтобы не быть голословным, внутри пример из того с чем имею дело.
Отлично в целом задуманные (и неплохо реализованные) Apache Kafka & ZooKeeper.
— Конфигурация? Но это же скучно! Тяп-ляп xml (потому что «из коробки»).
— Ой, а вы ещё и ACL хотите? Но это так муторно! Тап-ляп… Как-то так.
А в моей работе — ровно наоборот. Правильно (увы, с первого раза почти никогда нет) построенная модель позволяет дальше легко и непринуждённо (ну, почти) собрать схему.
Попадалась недавно на Хабре статья про нелёгкую работу data-scientist'ов…
Оказывается, этот момент у них реализуется в полной мере. А в моей практике, как говорится, «версия лайт». Многотомные модели, матёрые программисты с ООП наперевес и т.д. — это всё потом появится, когда/если взлетит. А конструктору надо вот здесь и сейчас с чего-то начинать.
Потому что он (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.
А где забыл/ошибся (без этого — никак), очень надеюсь что внимательные читатели не поленятся ткнуть носом.