Korektor układów „hswitcher” dla Linux: krok drugi

Ponieważ poprzedni post (xswitcher w fazie «proof of concept») otrzymał wystarczająco dużo konstruktywnych opinii (co jest przyjemne), kontynuowałem spędzanie swojego wolnego czasu na rozwijanie projektu. Teraz chcę poświęcić trochę waszego... Drugi krok będzie nieco nietypowy: propozycja/dyskusja na temat projektu konfiguracji.

Korektor układów „hswitcher” dla Linux: krok drugi

Jakoś tak wychodzi, że normalnym programistom konfigurowanie tych wszystkich pokręteł niesamowicie się nudzi.

Aby nie być gołosłownym, oto przykład tego, z czym mam do czynienia.
Ogólnie dobrze zaprojektowane (i niezłe w realizacji) Apache Kafka & ZooKeeper.
— Konfiguracja? Ale to nudne! Rób to na szybko w xml (bo „z pudełka”).
— Ojej, a chcecie jeszcze ACL? To takie męczące! Rób to na szybko… Jakoś tak.

A w mojej pracy — na odwrót. Poprawnie (niestety, rzadko udaje się to za pierwszym razem) stworzy model, co pozwoli dalej łatwo i swobodnie (no, prawie) zebrać schemat.

Ostatnio natknąłem się na artykuł na Habrze dotyczący trudnej pracy data-scientistów…
Okazuje się, że ten moment u nich jest w pełni realizowany. A w mojej praktyce, jak to się mówi, „lite wersja”. Wielotomowe modele, zaawansowani programiści z OOP w ręku itd. — to wszystko później się pojawi, gdy/jeśli wystartuje. A konstruktor musi tu i teraz od czegoś zacząć.

Wracając do tematu. Jako składnik syntaktyczny wziąłem TOML od tego pana.

Ponieważ on (TOML) z jednej strony jest przyjazny do edytowania przez człowieka. A z drugiej — jest transliterowany 1:1 do każdego z bardziej popularnych syntaksów: XML, JSON, YAML.
Co więcej, moja użyta implementacja z „github.com/BurntSushi/toml” chociaż nie jest najmodniejsza (wciąż syntaks 1.4), to syntaktycznie jest zgodna z tym samym („wbudowanym”) JSON.

Zatem, jeśli się chce, można po prostu powiedzieć „idź sobie z tym swoją TOML, chcę XXX” i „poprawić” kod jednym wierszem.

W ten sposób, jeśli zdecyduję się napisać jakieś okna do konfiguracji xswitchera (na pewno nie ja) nie przewiduje się problemów z „tym waszym cholernym configiem”.

Dla wszystkich innych syntaktyka oparta na „klucz = wartość” (i dosłownie z paroma trudniejszymi opcjami, typu = [jakiś, tam, tablica]) uważam
za intuicyjnie wygodną.
Co ciekawe, sam miał „gorączkę” mniej więcej w tym samym czasie (około 2013 roku). Tylko, w przeciwieństwie do mnie, autor TOML podszedł do tego z odpowiednim rozmachem.

Dlatego teraz łatwiej jest dostosować jego realizację do siebie, a nie odwrotnie.

W ogólnym zarysie bierzemy TOML (bardzo podobny do starego systemu INI). I definiujemy konfigurację, w której opisujemy, jak przymocować serię haków w zależności od zestawu ostatnich kodów skanera z klawiatury. Poniżej, kawałkami — to, co do tej pory udało się osiągnąć. I wyjaśnienia, dlaczego tak to wymyśliłem.

0. Podstawowe abstrakcje

  • Oznaczenia kodów skanera. Z tym należy coś zrobić, ponieważ same cyfrowe kody są zupełnie nieczytelne dla ludzi (to ja w ogrodzie loloswitcher).
    Wyciągnąłem „ecodes.go” z „golang-evdev” (nie chciało mi się zaglądać do źródła, chociaż autor dobrze to wskazał). Trochę (na razie) poprawiłem naprawdę straszne. Na przykład „LEFTBRACE” → „L_BRACE”.
  • Dodatkowo wprowadziłem pojęcie „klawiszy ze stanem”. Ponieważ użyta gramatyka regularna nie sprzyja długim pasmom. (Zamiast tego pozwala na sprawdzanie przy minimalnych obciążeniach. Jeśli używać tylko „bezpośredniego” zapisu.)
  • Będzie wbudowany „deduplikaotor” naciśnięć. W ten sposób stan „powtórz”=2 będzie zapisany jeden raz.

1. Sekcja szablonów

[Templates] # "@name@" do uproszczenia wyrażeń
 # Słowa mogą składać się z tych znaków (regex)
 "WORD" = "([0-9A-Z`;']|[LR]_BRACE|COMMA|DOT|SLASH|KP[0-9])"

Z czego składa się słowo języka ludzkiego z fonetycznym zapisem (to zupełnie inne sprawy, jak grafemy aka „hieroglify”)? Какая-то ужасная «простыня». Поэтому сразу закладываю понятие «шаблона».

2. Co robić, kiedy coś nacisnęliśmy (przyszedł nowy kod skanera)

[ActionKeys]
 # Zbieraj klawisz i testuj sekwencję poleceń
 # !!! Powtarzające się kody (code=2) muszą być zbierane raz na klawisz!
 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"]

 # Usuń wszystkie zebrane klawisze, w tym ten. To jest akcja domyślna.
 Drop = ["ESC", "-", "TAB", "ENTER", "KPENTER", "LINEFEED..POWER"]
 # Przechowuj dodatkową mapę dla tych klawiszy, gdy jakikolwiek jest w stanie "down".
 # Stan jest sprawdzany za pomocą warunków "OFF:"|"ON:" w akcji.
 # (Dodatkowo, stan tych klawiszy musi się utrzymywać między zrzutami buforów.)
 # ??? Jak radzić sobie z klawiszami CAPS i "LOCK"???
 StateKeys = ["L_CTRL", "L_SHIFT", "L_ALT", "L_META", "CAPS", "N_LOCK", "S_LOCK",
              "R_CTRL", "R_SHIFT", "R_ALT", "R_META"]

 # Test tylko, ale nie zbieraj.
 # Na przykład, używam F12 zamiast BREAK na głupich laptopach z kiepskimi klawiaturami (nowe ThinkPady)
 Test = ["F1..F10", "ZENKAKUHANKAKU", "102ND", "F11", "F12",
          "RO..KPJPCOMMA", "SYSRQ", "SCALE", "HANGEUL..YEN",
          "STOP..SCROLLDOWN", "NEW..MAX"]

Łącznie przewidziano 768 kodów. (Ale „na wszelki wypadek” włączyłem do kodu xswitcher przechwytywanie „niespodzianek”).
Wewnątrz opisałem wypełnienie tablicy odnośnikami do funkcji „co robić”. W golang to (niespodziewanie) okazało się wygodne i oczywiste.

  • Planuję zminimalizować "Drop" w tym miejscu. Na rzecz bardziej elastycznego przetwarzania (pokażę poniżej).

3. Tabela z klasami okien.

# 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"

Wiersze tabeli — w podwójnych kwadratowych nawiasach z jej nazwą. Na początku nie udało się prościej. W zależności od aktualnie aktywnego okna, można dopasować opcje:

  • Mój zestaw „gorących klawiszy” „Actions = …”. Jeśli brak/pusty — nie rób nic.
  • Przełącznik „MouseClickDrops” — co zrobić po wykryciu kliknięcia myszką. Ponieważ w punkcie włączenia xswitcher nie ma żadnych szczegółów „gdzie tam klikają”, domyślnie resetujemy bufor. Ale w terminalach (na przykład) można tego nie robić. (zazwyczaj).

4. Jeden (lub kilka) sekwencji naciśnięć uruchamia dany hook.

# 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)" ]

Podzieliłem hooki na dwa typy. Wbudowane, z „mówiącymi” nazwami (NewWord, NewSentence, Compose) i programowalne.

Nazwy programowalnych zaczynają się od „Action.”. Ponieważ TOML v1.4, nazwy z kropkami muszą być w cudzysłowach.

Poniżej dla każdego musi być opisany przekrój o takiej samej nazwie..

Aby nie eksplodować ludziom umysły „gołymi” wyrażeniami regularnymi (z doświadczenia, ich napisanie-to może jeden na dziesięciu profesjonalistów), od razu wdrażam dodatkową składnię.

  • „OFF:” (lub „ON:”) przed regexp (wyrażeniem regularnym) wymagają, aby wskazane później klawisze były zwolnione (lub naciśnięte).
    Następnie zamierzam stworzyć „niesprawiedliwe” wyrażenie regularne. Z oddzielną kontrolą kawałków między pipe'ami "|". W celu zmniejszenia liczby rejestrów typu "[LR]_SHIFT" (gdzie to wyraźnie nie jest potrzebne).
  • „SEQ:” Jeśli poprzedni warunek jest spełniony (lub nieobecny), następnie sprawdzamy w odniesieniu do „zwykłego” wyrażenia regularnego. Z detalami odsyłam od razu do biblioteki „regexp”. Ponieważ sam nadal nie zdążyłem ustalić stopnia zgodności z moimi ulubionymi pcre („perl compatible”).
  • Wyrażenie zapisuje się w formie „KLAWISZ_1: KOD1, KLAWISZ_2: KOD2” itd., w porządku przyjmowania kodów skanera.
  • Sprawdzenie zawsze „przyczepia się” do końca sekwencji, dlatego nie trzeba dodawać "$" na końcu.
  • Wszystkie kontrole w jednej linii są wykonywane jedna po drugiej i łączą się poprzez „I”. Ale ponieważ wartość opisana jest w formie tablicy, można po przecinku napisać alternatywną kontrolę. Jeśli to z jakiegoś powodu potrzebne.
  • Wartość „SeqLength = 8” ogranicza rozmiar bufora, w odniesieniu do którego wykonywane są wszystkie kontrole. Ponieważ w moim życiu (do tej pory) nie spotkałem nieskończonych zasobów.

5. Przypisanie haków opisanych w poprzedniej sekcji

# 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.

Główne tutaj — „Action = [Tablica]”. Podobnie jak w poprzedniej sekcji, jest ograniczony zestaw wbudowanych działań. I nieograniczona zasadniczo możliwość łączenia (napisz „Action.XXX” i nie zapomnij opisać pod nim jeszcze jednej sekcji).
W tym, przekształcenie słowa w poprawnej układzie dzieli się na dwie części: „zmień układ jak tam zadano” i „przepisz” („RetypeWord”).

Pozostałe parametry są zapisywane w „słowniku” („map” w golang) dla danego działania, ich lista zależy od tego co napisano w „Action”.

Kilka różnych działań można opisać w jednym stosie (sekcjach). Można je też rozdzielić. Jak pokazałem wcześniej.

Od razu zakładam działanie „Exec” — wykonaj zewnętrzny skrypt. Z opcją przesłania do niego w stdin zapisanego bufora.

  • „Wait = 1” — czekać na zakończenie uruchomionego procesu.
  • Prawdopodobnie, „do tego” zechcesz w otoczeniu ustawiać dodatkowe informacje, takie jak nazwa klasy okna, z którego zostało przechwycone.
    „Chcesz podłączyć swój handler? Tutaj masz.”

Uff (odetchnąłem). Wygląda na to, że niczego nie zapomniałem.

Hop! Aha, nie zapomniałem…
A gdzie konfiguracja uruchamiania? W hardcodzie? Oto przykładowo:

[ScanDevices]
 # Musi istnieć na starcie. Samoodtwarzanie, jeśli jest młodsze niż 30s
 Test = "\/dev\/input\/event0"
 Respawn = 30
 # Maski wyszukiwania
 Search = "\/dev\/input\/event*"
 # Na moich thinkPadach są takie pseudo-klawiatury z mnóstwem zbędnych zdarzeń
 Bypass = "(?i)Video|Camera" # „(?i)” wyraźnie różni się od klasycznego pcre.

A gdzie zapomniałem/popełniłem błąd (bez tego — ani rusz), mam nadzieję, że uważni czytelnicy nie będą mieli oporów, by pokazać mi to.

Powodzenia!

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster