Коректор розкладок "х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. Розділ шаблонів

[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

Додати коментар або відгук