Тъй като (х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). И задаваме конфигурация, в която описваме как да свържем серия от хукове в зависимост от набора от последни скен-кодове от клавиатурата. По-долу на парчета — това, което засега сме постигнали. И обяснения на това, защо реших така.
0. Основни абстракции
- Обозначения на скен-кодовете. С това задължително трябва да се направи нещо, тъй като просто цифровите кодове абсолютно не са четими за човека (това ми е за градината) ).
Изразих "ecodes.go" от "golang-evdev" (не ми се стори нужно да бъркам в първоизточника, макар че авторът му е посочен доста културно). Малко (засега) поправих съвсем драстичните неща. Например "LEFTBRACE" → "L_BRACE". - Допълнително въведох понятието "клавиши със състояние". Тъй като използваната регулярна граматика не предразполага към дълги пасажи. (Но позволява проверка с минимални разходи. Ако се използва само "прямо" записване.)
- Ще има вграден "дедупликатор" на натисканията. По този начин, състоянието "повтор"=2 ще бъде записано един път.
1. Раздел на шаблоните
[Templates] # "@name@" за опростяване на изразите
# Думите могат да се състоят от тези знаци (regex)
"WORD" = "([0-9A-Z`;']|[LR]_BRACE|COMMA|DOT|SLASH|KP[0-9])"
От какво се състои дума от човешкия език с фонетична транскрипция (не е като графемите aka „йероглифи“)? Какая-то ужасная «простыня». Поэтому сразу закладываю понятие «шаблона».
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:" Ако предишното условие е изпълнено (или отсъства), проверяваме относно "обикновеното" регулярното изражение. За подробности веднага изпращам на^Wв библиотеката "regexp". Защото сам все още не съм се отбрани да изясня степента на съвместимост с моите любими pcre ("perl compatible").
- Изражението се записва под формата "BUКONTA_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]
# Трябва да съществува при стартиране. Автоматично възстановяване, ако е по-млад от 30s
Test = "\/dev\/input\/event0"
Respawn = 30
# Търсене на маска
Search = "\/dev\/input\/event*"
# В моите ThinkPads има такива псевдоклавиатури с безброй ненужни събития
Bypass = "(?i)Video|Camera" # "(?i)" очевидно се различава от "класическите" pcre.
А къде забравих/сбърках (без това — никак), много се надявам, че внимателните читатели няма да се мързелуват да посочат.
Успех!
Източник: habr.com
