Da (хswitcher at the "proof of concept" stage) received quite a few constructive reviews (which is nice), I continued to spend my free time developing the project. Now I want to spend a little of yours… The second step will be somewhat unusual: proposal/discussion of the configuration design.

It somehow turns out that normal programmers find it incredibly boring to configure all these knobs.
To avoid being unfounded, here’s an example of what I’m dealing with.
Well thought out (and decently implemented) Apache Kafka & ZooKeeper.
— Configuration? But that’s boring! Quick-and-dirty XML (because it’s "out of the box").
— Oh, and you even want ACL? But that’s so tedious! Just slap it together… Something like that.
And in my work, it’s exactly the opposite. Correctly built model allows for easy and natural (well, almost) to assemble the scheme. Recently came across an article on Habr about the difficult work of data scientists… It turns out this moment is fully realized for them. And in my practice, as they say, "light version." Multi-volume models, seasoned programmers with OOP expertise, etc. — all of this will come later, when/if it takes off. But the constructor needs to start with something right here and now.
Closer to the point. As a syntactic basis, I chose TOML
from this citizen
Because it’s .
on one hand, human-readable and editable. On the other hand, it translates 1:1 into any of the more common syntaxes: XML, JSON, YAML. Moreover, the implementation I used from "github.com/BurntSushi/toml" although not the trendiest (syntax 1.4 is still in use), is syntactically compatible with the same ("built-in") JSON. That is, if desired, one can simply say "go to hell with your TOML, I want XXX" and "patch" the code with just one line.
Thus, if one wishes to write some windows for configuring хswitcher
(definitely not me)
the problems "with this damn config" — are not anticipated. For all others, the syntax based on "key = value" (and literally a couple of options that are a little more complex, like = [some, array])
I believe intuitively convenient. Interestingly, the author himself
"got burned"
around the same time (around 2013). Only, unlike me, the author of TOML approached it with due grandeur. 물론 ;;;
Derfor er det nu lettere for mig at tilpasse implementeringen til mig selv, end omvendt.
Generelt tager vi TOML (meget lig den gamle Windows INI). Og vi opstiller konfigurationen, hvor vi beskriver, hvordan man tilslutter en række hooks afhængigt af det seneste sæt skancode fra tastaturet. Nedenfor i stykker - det, jeg indtil videre har fået til at fungere. Og forklaringer på, hvorfor jeg har besluttet mig for dette.
0. Grundlæggende abstraktioner
- Betegnelser for skancode. Der skal gøres noget ved dette, da blot numeriske koder absolut ikke er menneskeligt læsbare (det er jeg i haven) ).
Tog "ecodes.go" fra "golang-evdev" (jeg gad ikke røre ved kildeoplysningerne, selvom forfatteren oplyser det pænt). Jeg har lidt (indtil videre) rettet det meget frygtelige. Som "LEFTBRACE" → "L_BRACE". - Desuden har jeg indført begrebet "taster med tilstand". Da den anvendte regelmæssige grammatik ikke egner sig til lange passager. (Til gengæld tillader det at kontrollere med minimale omkostninger. Hvis man kun bruger "direct" optagelse.)
- Der vil være en indbygget "deduplicator" for tryk. På denne måde vil tilstanden "gentag"=2 blive registreret en gang.
1. Skabelonsektioner
[Templates] # "@name@" for at simplificere udtryk
# Ord kan bestå af disse karakterer (regex)
"WORD" = "([0-9A-Z`;']|[LR]_BRACE|COMMA|DOT|SLASH|KP[0-9])"
Hvad et ord i et menneskesprog består af med fonetisk optegnelse (det er noget andet end grafemer aka "skrifttegn")? Какая-то ужасная «простыня». Поэтому сразу закладываю понятие «шаблона».
2. Hvad man skal gøre, når der trykkes på noget (når et nyt skancode ankommer)
[ActionKeys]
# Indsaml tast og udfør testen for kommando-sekvens
# !!! Gentagelseskoder (code=2) skal indsamles én gang pr. 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"]
# Drop alle indsamlede taster, inklusive denne. Dette er standardhandlingen.
Drop = ["ESC", "-", "TAB", "ENTER", "KPENTER", "LINEFEED..POWER"]
# Gem ekstra kort for disse taster, når nogen er i "down" tilstand.
# Tilstanden kontrolleres via "OFF:"|"ON:" betingelser i handlingen.
# (Også, tilstanden for disse taster skal opretholdes mellem buffer drops.)
# ??? Hvordan skal man håndtere CAPS og "LOCK"-taster???
StateKeys = ["L_CTRL", "L_SHIFT", "L_ALT", "L_META", "CAPS", "N_LOCK", "S_LOCK",
"R_CTRL", "R_SHIFT", "R_ALT", "R_META"]
# Test kun, men indsamle ikke.
# For eksempel bruger jeg F12 i stedet for BREAK på dumme bærbare computere med dårlige tastaturer (nye ThinkPads)
Test = ["F1..F10", "ZENKAKUHANKAKU", "102ND", "F11", "F12",
"RO..KPJPCOMMA", "SYSRQ", "SCALE", "HANGEUL..YEN",
"STOP..SCROLLDOWN", "NEW..MAX"]
Der er i alt 768 koder. (Men "for en sikkerheds skyld" indsatte jeg en kode til xswitcher til fangst af "overraskelser").
Inden for beskrev jeg udfyldningen af arrayet med links til funktioner "hvad skal der gøres". På golang er det (pludselig) vist sig at være bekvemt og indlysende.
- Jeg planlægger at reducere "Drop" her til et minimum. Til fordel for en mere fleksibel behandling (viser nedenfor).
3. Tabel med vinduesklasser
# 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ækker i tabellen — i dobbelte firkantede parenteser med dens navn. Det var ikke så nemt fra starten. Afhængig af det nuværende aktive vindue kan man vælge indstillinger:
- Min egen samling af "genvejstaster" "Actions = …". Hvis der ikke er noget/er tomt — gør ingenting.
- Veksleren "MouseClickDrops" — hvad der skal gøres ved registrering af et museklik. Da der ikke er nogen detaljer ved aktiveringspunktet xswitcher om "hvor man klikker", nulstiller vi bufferet som standard. Men i terminaler (for eksempel) kan man vælge ikke at gøre det. (som regel).
4. En (eller flere) sekvenser af tryk aktiverer en eller anden 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)" ]
Hooks er opdelt i to typer. Indbyggede, med "sigende" navne (NewWord, NewSentence, Compose) og programmérbare.
Navne for de programmérbare starter med "Action.". Fra og med TOML v1.4 skal navne med punktummer være i citationstegn.
Nedenfor skal der beskrives et afsnit for hver med det samme navn.
For ikke at overbelaste folk med "nøgne" regulære udtryk (baseret på erfaring, deres skrive-kan være en ud af ti professionelle), introducerer jeg straks yderligere syntaks.
- "OFF:" (eller "ON:") foran regexp (regulært udtryk) kræver, at de angivne knapper derefter er blevet sluppet (eller trykket).
Jeg planlægger også at lave et "uærligt" regulært udtryk. Med separat kontrol af stykkerne mellem pipe-tegnene "|". For at reducere antallet af poster som "[LR]_SHIFT" (hvor det tydeligt ikke er nødvendigt). - "SEQ:" Hvis den forrige betingelse er opfyldt (eller fraværende), kontrolleres der derefter i forhold til et "almindeligt" regulært udtryk. For flere detaljer henviser jeg straks til biblioteket "regexp". Fordi jeg stadig ikke har taget mig tid til at finde ud af graden af kompatibilitet med mine yndlings pcre ("perl kompatibel").
- Udtrykket skrives som "KNOBA_1: KODE1, KNOBA_2: KODE2" og så videre, i rækkefølge af indkomne scankoder.
- Kontrollen "trykkes" altid mod slutningen af sekvensen, så "$" behøver ikke at blive tilføjet i enden.
- Alle kontroller i én linje udføres efter hinanden og kombineres med "OG". Men da værdien er beskrevet som et array, kan man efter kommaet skrive et alternativt kontrol. Hvis det er nødvendigt.
- Værdi "SeqLength = 8" begrænser størrelsen på bufferen, i forhold til hvilken alle kontroller udføres. Da jeg i mit liv (indtil nu) ikke har mødt uendelige ressourcer.
5. Tildeling af hooks, som beskrevet i forrige sektion
# 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.
Det vigtigste her er — «Action = [Array]». Ligesom i den forrige sektion er der et begrænset antal indbyggede handlinger. Og i princippet er der ingen begrænsninger på integration. (skriv «Action.XXX» og vær ikke doven med at beskrive endnu en sektion for det).
Ikke mindst skal omdefineringen af ord i den rette layout opdeles i to dele: «ændre layoutet som angivet derovre» og «gengiv» («RetypeWord»).
De øvrige parametre skrives ind i «ordbogen» («map» i golang) for denne handling, listen afhænger af det, der er skrevet i «Action».
Flere forskellige handlinger kan beskrives i en bunke (sektioner). Eller man kan adskille dem. Som jeg viste før.
Jeg indfører straks handlingen «Exec» — udfør et eksternt script. Med muligheden for at dirigere den ind i stdin den indspillede buffer.
- «Wait = 1» — vent på, at den kørende proces afsluttes.
- Formentlig vil man «til sammen» gerne sætte ekstra information i miljøet, som klassens navn fra hvilket der blev opsnappet.
«Vil du tilslutte din handler? Her er linket.»
Puh (jeg lettede vejret). Det ser ud til, at jeg ikke har glemt noget.
Ups! Aha, jeg glemte ikke…
Og hvor er opstartskonfigurationen? I hardkoden? Cirka sådan:
[ScanDevices]
# Skal eksistere ved opstart. Selvrestart, hvis den er yngre end 30s
Test = "\/dev\/input\/event0"
Respawn = 30
# Søg masker
Search = "\/dev\/input\/event*"
# På mine thinkPads er der sådanne pseudo-tastaturer med tonsvis af unødvendige begivenheder
Bypass = "(?i)Video|Camera" # "(?i)" adskiller sig åbenlyst fra "klassisk" pcre's.
Og hvor glemte jeg/fejlede jeg (uden dette — intet), jeg håber virkelig, at de opmærksomme læsere ikke vil være dovne med at pege det ud.
Held og lykke!
Kilde: habr.com
