Deoarece (hswitcher în faza de „dovadă de concept”) a primit destul de multe feedback-uri constructive (ceea ce este plăcut), am continuat să-mi petrec timpul liber dezvoltând proiectul. Acum vreau să folosesc puțin din timpul vostru… Al doilea pas va fi ceva mai neobișnuit: propunerea/discuția despre designul configurației.

Se pare că programatorilor normali le este extrem de plictisitor să regleze toate aceste butoane.
Pentru a nu fi nedefinit, iată un exemplu din ceea ce mă confrunt.
Excelent concepute în general (și realizate destul de bine) Apache Kafka & ZooKeeper.
— Configurare? Dar este atât de plictisitor! Făcând xml într-un mod rapid (pentru că „din cutie”).
— Oh, dar tu vrei și ACL? Dar este atât de complicat! Tăiat și lipit… Așa-i.
În munca mea este exact invers. Corect (din păcate, aproape niciodată nu este corect din prima) modelul construit permite apoi să se reunească ușor și fără stres (ei bine, aproape) schema.
Am dat recent peste un articol pe Habr despre munca dificilă a data-scientist-ilor…
Se pare că acest moment este realizat pe deplin de ei. Și în practica mea, cum s-ar spune, „versiunea light”. Modele voluminoase, programatori experimentați cu OOP și așa mai departe — toate acestea vor apărea mai târziu, când/ dacă va decola. Dar constructorul trebuie să înceapă cu ceva aici și acum.
Să revenim la subiect. Ca bază sintactică, am folosit TOML .
Pentru că el (TOML) pe de o parte este editabil de către oameni. Iar pe de altă parte — se traduce 1:1 în orice din sintaxele mai răspândite: XML, JSON, YAML.
Mai mult, implementarea pe care am folosit-o de la „github.com/BurntSushi/toml” deși nu este cea mai modernă (încă sunt la sintaxa 1.4), este totuși compatibilă sintactic cu aceeași („înglobată”) JSON.
Asta înseamnă că, dacă îți dorești, poți pur și simplu să spui „du-te dracului cu TOML-ul tău, eu vreau XXX” și „să modifici” codul cu un singur rând.
Astfel, dacă vrei să scrii pentru configurarea hswitcher niște feronerie (cu siguranță nu eu) problemele cu „acest dolban config-ul vostru” — nu sunt prevăzute.
Pentru toți ceilalți, sintaxa bazată pe „cheie = valoare” (și literal cu câteva opțiuni mai complicate, de genul = [unui, anumit, array]) cred
intuitiv convenabil.
Ceea ce este curios, eu însumi aproape în același timp (pe la 2013). Doar că, spre deosebire de mine, autorul TOML a intrat cu un impact corespunzător.
De aceea, acum îmi este mai ușor să îmi adaptez implementarea în loc să fac invers.
În general, folosiți TOML (foarte asemănător cu vechiul INI de Windows). Și stabilește configurația în care descriem cum să asociem o serie de hook-uri în funcție de setul de ultimele coduri de scanare de la tastatură. Mai jos, pe bucăți, se află ceea ce am realizat până acum. Și explicațiile pentru care am decis așa.
0. Abstracții de bază
- Semnele codurilor de scanare. Cu acesta trebuie neapărat să facem ceva, deoarece codurile numerice pur și simplu nu sunt ușor de citit (asta este explicația) ).
Am extras „ecodes.go” din „golang-evdev” (nu m-am obosit să mă uit în sursa principală, deși autorul o menționează destul de franc). Am corectat puțin (deocamdată) ceea ce era complet inacceptabil. De exemplu, „LEFTBRACE” → „L_BRACE”. - Am introdus suplimentar conceptul de „taste cu stare”. Deoarece gramatică regulată utilizată nu permite pasaje lungi. (Dar permite verificarea cu cheltuieli minime. Dacă utilizăm doar înregistrarea directă.)
- Va exista un „deduplicator” integrat pentru apăsările efectuate. Astfel, starea „repetare”=2 va fi înregistrată. una de ori.
1. Secțiunea template-urilor
[Templates] # "@name@" pentru a simplifica expresiile
# Cuvintele pot consta din acești caractere (regex)
"WORD" = "([0-9A-Z`;']|[LR]_BRACE|COMMA|DOT|SLASH|KP[0-9])"
Din ce este format un cuvânt în limba umană cu transcriere fonetică ( comparativ cu grafemele aka „hieroglife”)? Какая-то ужасная «простыня». Поэтому сразу закладываю понятие «шаблона».
2. Ce să facem când s-a apăsat ceva (a venit un nou cod de scanare)
[ActionKeys]
# Colectați cheia și faceți testul pentru secvența de comenzi
# !!! Codurile repetitive (code=2) trebuie colectate o dată per tastă!
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"]
# Eliminați toate cheile colectate, inclusiv aceasta. Aceasta este acțiunea implicită.
Drop = ["ESC", "-", "TAB", "ENTER", "KPENTER", "LINEFEED..POWER"]
# Stocați un harta suplimentară pentru aceste taste, când oricare este în starea "down".
# Starea este verificată prin condițiile "OFF:"|"ON:" în acțiune.
# (De asemenea, starea acestor taste trebuie să persiste între eliminările buffer-ului.)
# ??? Cum să facem față tastelor 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 doar, dar nu colectați.
# De exemplu, eu folosesc F12 în loc de BREAK pe laptopuri dumb cu tastaturi proaste (noile ThinkPads)
Test = ["F1..F10", "ZENKAKUHANKAKU", "102ND", "F11", "F12",
"RO..KPJPCOMMA", "SYSRQ", "SCALE", "HANGEUL..YEN",
"STOP..SCROLLDOWN", "NEW..MAX"]
În total, sunt prevăzute 768 de coduri. (Dar „pentru orice eventualitate” am inclus în codul hswitcher capturarea „surprizelor”).
În interior, am detaliat completarea array-ului cu referințe la funcții „ce să facem”. Pe golang, acest lucru (întâmplător) s-a dovedit a fi convenabil și evident.
- Planific ca „Drop” în acest loc să fie redus la minim. În favoarea unei procesări mai flexibile (voi arăta mai jos).
3. Tabelul cu clasele feronerie
# 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"
Rândurile tabelului sunt în paranteze pătrate duble cu numele său. Mai simplu nu a fost posibil din prima. În funcție de fereastra activă curentă, se pot alege opțiuni:
- Setul tău de „taste rapide” „Actions = …”. Dacă nu există / este gol – nu face nimic.
- Comutatorul „MouseClickDrops” – ce să faci când se detectează un clic de mouse. Deoarece în punctul de activare xswitcher nu există detalii despre „unde se face clic”, resetăm bufferul din start. Dar în terminale (de exemplu) nu este obligatoriu să fă acest lucru. (de obicei).
4. Una (sau mai multe) secvențe de apăsare a tastelor declanșează un hook sau altul.
# 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)" ]
Hooks sunt împărțiți în două tipuri. Încapsulate, cu nume „vorbitoare” (NewWord, NewSentence, Compose) și programabile.
Numele celor programabile încep cu „Action.”. Din moment ce TOML v1.4, numele cu puncte trebuie să fie între ghilimele.
Mai jos, pentru fiecare ar trebui să fie descrisă o secțiune cu același nume..
Pentru a nu exploda mințile oamenilor cu regex-uri „goale” (din experiență, acestea a scrie-pot fi una din zece profesioniști), introduc imediat o sintaxă suplimentară.
- „OFF:” (sau „ON:”) în fața regexp (expresie regulată) necesită ca butoanele specificate ulterior să fie eliberate (sau apăsate).
Mai departe, intenționez să creez o expresie regulată „necinstită”. Cu verificarea separată a porțiunilor între pipe-uri "|". Scopul este de a reduce numărul de înregistrări de tipul "[LR]_SHIFT" (acolo unde nu este evident necesar). - „SEQ:” Dacă condiția anterioară este îndeplinită (sau lipsă), verificăm în legătură cu o expresie regulată „obișnuită”. Pentru detalii, trimit imediat la biblioteca „regexp”. Pentru că eu însumi nu m-am obosit să aflu gradul de compatibilitate cu pc-rele mele preferate (compatibile perl).
- Expresia se scrie sub forma „BUTTON_1: CODE1, BUTTON_2: CODE2” și așa mai departe, în ordinea sosirii codurilor scanate.
- Verificarea este întotdeauna „apăsată” la sfârșitul secvenței, așa că nu este nevoie să adaugăm "$" la coada.
- Toate verificările pe aceeași linie se desfășoară una după alta și se combină prin „ȘI”. Dar, deoarece valoarea este descrisă sub formă de masă, se poate scrie o verificare alternativă după virgulă. Dacă este necesar pentru ceva.
- Valoare „SeqLength = 8” limitează dimensiunea buffer-ului, în raport cu care se efectuează toate verificările. Deoarece în viață nu am întâlnit (până acum) resurse infinite.
5. Asignarea hook-urilor, descrise în secțiunea anterioară.
# 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.
Elementul principal aici este— «Action = [Array]». Similar cu secțiunea anterioară, există un set limitat de acțiuni încorporate. Și o posibilitate nelimitată, în principiu, de interconectare. (scrie «Action.XXX» și nu ezita să explici o altă secțiune pentru acesta).
Inclusiv, reasocierea cuvântului în layoutul corect este împărțită în două părți: «schimbă layoutul așa cum este specificat acolo» și «rescrie» («RetypeWord»).
Celelalte parametri sunt înregistrate în „dicționar” („map” în golang) pentru această acțiune, lista lor depinde de ceea ce este scris în „Action”.
Mai multe acțiuni diferite pot fi descrise într-un singur grup (secțiuni). Sau pot fi despărțite. Așa cum am arătat mai sus.
Imediat îmi vin în minte acțiunea „Exec” — a executa un script extern. Cu opțiunea de a-i aduce în stdin un buffer înregistrat.
- „Wait = 1” — așteaptă finalizarea procesului lansat.
- Probabil că „pentru diversitate” va fi util să expui în mediu informații suplimentare precum numele clasei feron degrevat.
„Vrei să-ți conectezi propriul handler? Ai nevoie de asta.”
Uf (am răsuflat). Părea că nu am uitat nimic.
Ops! Ah, nu am uitat…
Dar unde este configurarea de lansare? În codul hardat? Cam așa:
[ScanDevices]
# Trebuie să existe la start. Auto-reîncarnare în caz că este mai tânăr de 30s
Test = "\/dev\/input\/event0"
Respawn = 30
# Masca de căutare
Search = "\/dev\/input\/event*"
# În laptopurile mele thinkPad există astfel de pseudo-claviaturi cu tone de evenimente inutile
Bypass = "(?i)Video|Camera" # "(?i)" evident diferă de pcre-urile „clasice”.
Dar unde am uitat/greșit (fără asta — nu se poate), sper foarte mult că cititorii atenți nu se vor sfii să pună degetul pe hârtie.
Mult noroc!
Sursa: habr.com
