Ponieważ (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.

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