Карэктар раскладак "хswitcher" для linux: крок другі

Так як папярэдняя публікацыя (хswitcher на стадыі "proof of concept") атрымала дастаткова шмат канструктыўных водгукаў (што прыемна), я працягнуў марнаваць свой вольны час на развіццё праекта. Цяпер хачу патраціць крышку вашага… Другі крок будзе не зусім звыклы: прапанова/абмеркаванне дызайну канфігурацыі.

Карэктар раскладак "х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 (вельмі падобны на стары віндовы INI). І стаім канфігурацыю, у якой апісваем як прычапіць серыю хукаў у залежнасці ад набору апошніх скен-кодаў з клавіятуры. Ніжэй па кавалках – тое што цяпер атрымалася. І тлумачэнні чаго гэта я так вырашыў.

0. Базавыя абстракцыі

  • Абазначэнні скен-кодаў. З гэтым абавязкова трэба нешта рабіць, бо проста лічбавыя коды абсалютна не чалавека-чытэльныя (гэта я ў агарод. loloswitcher).
    Вытрас "ecodes.go" з "golang-evdev" (у першакрыніцу лезці паленаваўся, хоць у аўтара ён суцэль культурна паказаны). Трохі (пакуль) паправіў зусім ужо страхалюднае. Тыпу "LEFTBRACE" → "L_BRACE".
  • Дадаткова ўвёў паняцце "клавіш са станам". Бо выкарыстаная рэгулярная граматыка не размяшчае да доўгіх пасажаў. (Затое дазваляе правяраць з мінімальнымі накладкамі. Калі выкарыстоўваць толькі "прамы" запіс.)
  • Будзе ўбудаваны "дэдуплікатар" націснутага. Такім чынам, стан "паўтор"=2 будзе запісана 1 раз.

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:» Калі папярэдняя ўмова выканана (ці адсутнічае), далей правяраем адносна "звычайнага" рэгулярнага выразы. Па падрабязнасці адразу пасылаю на бібліятэку «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.

А дзе забыўся/памыліўся (без гэтага - ніяк), вельмі спадзяюся што ўважлівыя чытачы не палянуюцца ткнуць носам.

Ўдачы!

Крыніца: habr.com

Дадаць каментар