
Как-то оно так получается, что нормальным программистам настраивать все эти крутилки дико скучно.
— Конфигурация? Но это же скучно! Тяп-ляп xml (потому что «из коробки»).
— Ой, а вы ещё и ACL хотите? Но это так муторно! Тап-ляп… Как-то так.
А в моей работе — ровно наоборот. Правильно (увы, с первого раза почти никогда нет) построенная модель позволяет дальше легко и непринуждённо (ну, почти) собрать схему.
Ближе к делу. В качестве синтаксической основы я взял TOML вот от этого гражданина.
Потому что он (TOML) с одной стороны человеко-редактируемый. А с другой — транслируется 1:1 в любой из более распространённых синтаксисов: XML, JSON, YAML.
Более того, использованная мной реализация от «github.com/BurntSushi/toml» хотя и не самая модная (до сих пор синтаксис 1.4), зато синтаксически совместима с тем же («встроенным») JSON.
То есть, при желании можно просто сказать «иди лесом с этим твоим TOML, я хочу XXX» и «пропатчить» код всего одной строкой.
Таким образом, при желании написать для настройки хswitcher какие-то окошки (точно не я) проблем «с этим вашим долбаным конфигом» — не предвидится.
Для всех прочих синтаксис на основе «ключ = значение» (и буквально парой опций посложнее, типа = [какого, то, массива]) полагаю
Поэтому сейчас мне проще подстроить его реализацию под себя, а не наоборот.
В общем, берём 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.
А где забыл/ошибся (без этого — никак), очень надеюсь что внимательные читатели не поленятся ткнуть носом.
Удачи!
ссылка на оригинал статьи https://habr.com/ru/post/499560/
Добавить комментарий