Тъй като (хswitcher на етапа «доказателство за концепция») получи доста конструктивни отзиви (което е приятно), продължих да използвам свободното си време за развитие на проекта. Сега искам да похарча малко от вашето... Вторият стъпка ще бъде малко нетипичен: предложение/дискусия за дизайна на конфигурацията.

Както се оказва, на нормалните програмисти им е скучно да настройват тези бутони.
За да не бъда голословен, вътре е пример от това, с което имам работа.
Отлично замислени (и добре реализирани) 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 (много подобен на стария Windows INI). И задаваме конфигурация, в която описваме как да свържем серия от хукове в зависимост от набора от последни скен-кодовe от клавиатурата. По-долу, част по част — това, което до момента успяхме да направим. И пояснения, защо реших така.
0. Основни абстракции
- Обозначения на скен-кодовете. С това задължително трябва да се направи нещо, тъй като просто цифровите кодове не са четими за човек (това е, как да кажа), ).
Изтеглих "ecodes.go" от "golang-evdev" (не ми се занимаваше да проверявам първоизточника, макар че автората е посочен културно). Малко (засега) коригирах най-ужасяващото. Например "LEFTBRACE" → "L_BRACE". - Допълнително въведох понятието "клавиши с състояние". Тъй като използваната регулярна граматика не е предразположена към дълги пасажи. (Затова позволява проверка с минимални разходи. Ако се използва само "директна" запис.)
- Ще има вграден "дедупликатор" за натисканията. Така, състоянието "повтор"=2 ще бъде записано. един път.
1. Раздел на шаблоните
[Templates] # "@name@" за опростяване на изразите
# Думите могат да се състоят от тези символи (регулярно изразяване)
"WORD" = "([0-9A-Z`;']|[LR]_BRACE|COMMA|DOT|SLASH|KP[0-9])"
От какво се състои дума на човешкия език с фонетична транскрипция (не е като графемите, известни като "иероглифы")? Какая-то ужасная «простыня». Поэтому сразу закладываю понятие «шаблона».
2. Какво да се прави, когато нещо е натиснато (постъпил е нов скен-код)
[ActionKeys]
# Събирайте ключа и правете тест за последователност от команди
# !!! Повтарящите се кодове (code=2) трябва да се събират само веднъж за всеки ключ!
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 = ["ESC", "-", "TAB", "ENTER", "KPENTER", "LINEFEED..POWER"]
# Запишете допълнителна карта за тези ключове, когато някой е в състояние "надолу".
# Състоянието се проверява чрез условия "OFF:"|"ON:" в действието.
# (Също, състоянието на тези ключове трябва да се запази между изхвърлянията на буфера.)
# ??? Как да се справим с CAPS и "LOCK"-ключовете ???
StateKeys = ["L_CTRL", "L_SHIFT", "L_ALT", "L_META", "CAPS", "N_LOCK", "S_LOCK",
"R_CTRL", "R_SHIFT", "R_ALT", "R_META"]
# Тест, но не събирайте.
# Например, аз използвам F12 вместо BREAK на глупави лаптопи с некачествени клавиатури (нови 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:» Ако предишното условие е изпълнено (или отсъства), след това проверяваме относно «обикновеното» регулярнo изражение. За подробности веднага изпращам на^Wв библиотеката «regexp». Защото сам още не съм се удостоил да установя степента на съвместимост с любимите ми pcre («perl compatible»).
- Изразът се записва във вида «BUTTON_1: CODE1, BUTTON_2: CODE2» и т.н., в реда на получаване на скен-кодовете.
- Проверка винаги «се натиска» на края на последователността, затова не е нужно да добавяте "$" в края.
- Всички проверки в една редица се изпълняват една след друга и се обединяват с «И». Но тъй като стойността е описана във вид на масив, след запетая може да се напише алтернативна проверка. Ако това е нужно по някаква причина.
- Стойност «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]
# Трябва да съществува при стартиране. Самовъзраждане, ако е по-млад от 30 секунди
Test = "\/dev\/input\/event0"
Respawn = 30
# Маска за търсене
Search = "\/dev\/input\/event*"
# В моите thinkPads има такива псевдо-клавиатури с много ненужни събития
Bypass = "(?i)Video|Camera" # "(?i)" очевидно се различава от "класическите" pcre.
А къде забравих/сгреших (без това — няма как), много се надявам, че внимателните читатели няма да се колебаят да посочат.
Успех!
Източник: habr.com
