Коректор на разкладките „хswitcher“ за Linux: стъпка втора

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

Коректор на разкладките „хswitcher“ за Linux: стъпка втора

По някакъв начин се получава, че на нормалните програмисти настройването на всичките тези ръчки е ужасно скучно.

За да не бъда голословен, вътре е пример от това, с което се занимавам.
Отлично замислени (и добре реализирани) 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. Основни абстракции

  • Обозначения на скен-кодовете. С това задължително трябва да се направи нещо, тъй като просто цифровите кодове абсолютно не са четими за човека (това ми е за градината) loloswitcher).
    Изразих "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

Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри 🔥 Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри | ProHoster