Poiché (lo switcher è nella fase di «proof of concept») ha ricevuto molte recensioni costruttive (il che è piacevole), ho continuato a investire il mio tempo libero nello sviluppo del progetto. Ora voglio investire un po' del vostro... Il secondo passo sarà un po' inusuale: proposta/discussione del design della configurazione.

In qualche modo accade che ai programmatori normali risulti noioso configurare tutti questi controlli.
Per non rimanere vago, qui dentro c'è un esempio di ciò con cui ho a che fare.
Ottimamente concepito (e abbastanza bene realizzato) Apache Kafka & ZooKeeper.
— Configurazione? Ma è noioso! Tappa e via con xml (perché «pronto all'uso»).
— Oh, e volete anche ACL? Ma è un lavoro così tedioso! Tappa e via… Ecco come si fa.
Nella mia esperienza, è esattamente il contrario. Giustamente (purtroppo, raramente si ottiene giusto al primo colpo) un modello costruito permette di proseguire facilmente e senza sforzo (beh, quasi) per raccogliere lo schema.
Recentemente ho trovato su Habr un articolo sul difficile lavoro dei data scientist...
A quanto pare, per loro questo aspetto è completamente realizzato. Nella mia pratica, come si suol dire, è una «versione leggera». Modelli multivolume, programmatori esperti con OOP in mano, ecc. — tutto questo arriverà poi, quando/se decollerà. Ma il progettista deve iniziare qui e ora con qualcosa.
Torniamo al punto. Come base sintattica ho scelto TOML .
Perché è (TOML) da un lato facilmente modificabile dall'uomo. Dall'altro — traslitterato 1:1 in uno qualsiasi degli altri sinonimi più diffusi: XML, JSON, YAML.
Inoltre, l'implementazione che ho utilizzato da «github.com/BurntSushi/toml», pur non essendo la più di moda (la sintassi è ancora 1.4), è compatibile sintatticamente con lo stesso («integrato») JSON.
Cioè, qualora si desideri, si può semplicemente dire «vai a quel paese con il tuo TOML, voglio XXX» e «patchare» il codice con una sola riga.
Così, se si desidera scrivere per configurare lo switcher qualche finestra (sicuramente non io) non si prevedono problemi «con questa vostra dannata configurazione».
Per tutti gli altri, la sintassi basata su «chiave = valore» (e letteralmente con un paio di opzioni più complesse, tipo = [alcuni, array]) ritengo
intuitivamente comoda.
Curiosamente, a me circa nello stesso periodo (nel 2013). Solo che, a differenza di me, l'autore di TOML è entrato con il giusto slancio.
Perciò adesso mi è più facile adattare la sua implementazione a me stesso, e non viceversa.
In sintesi, prendiamo TOML (molto simile al vecchio INI di Windows). E definiamo la configurazione in cui descriviamo come collegare una serie di hook a seconda dell'insieme degli ultimi codici di scansione dalla tastiera. Di seguito ci sono i pezzi che ho ottenuto fino ad ora. E le spiegazioni sul perché ho preso queste decisioni.
0. Astratti di base
- Designazioni dei codici di scansione. Con questo bisogna assolutamente fare qualcosa, poiché i semplici codici numerici non sono affatto leggibili per gli esseri umani (questo lo dico per esperienza) ).
Ho estratto «ecodes.go» da «golang-evdev» (non ho voglia di andare nella fonte originale, anche se l'autore lo ha indicato in modo piuttosto chiaro). Ho apportato alcune piccole (per ora) correzioni a cose davvero brutte. Tipo «LEFTBRACE» → «L_BRACE». - Ho anche introdotto il concetto di «tasti con stato». Poiché la grammatica regolare utilizzata non si presta a passaggi lunghi. (Ma permette di verificare con minimi sovraccarichi. Se si utilizza solo la scrittura «diretta».)
- Ci sarà un «deduplicatore» integrato per le pressioni. In questo modo, lo stato «ripetizione»=2 sarà registrato una di volte.
1. Sezione dei modelli
[Templates] # "@name@" per semplificare le espressioni
# Le parole possono consistere in questi caratteri (regex)
"WORD" = "([0-9A-Z`;']|[LR]_BRACE|COMMA|DOT|SLASH|KP[0-9])"
Di cosa è composta una parola della lingua umana con notazione fonetica (e non parliamo delle grafemi aka «ideogrammi»)? Какая-то ужасная «простыня». Поэтому сразу закладываю понятие «шаблона».
2. Cosa fare quando si preme qualcosa (arrivato un nuovo codice di scansione)
[ActionKeys]
# Colleziona il tasto e fai il test per la sequenza di comandi
# !!! I codici ripetuti (code=2) devono essere raccolti una sola volta per tasto!
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"]
# Scarta tutte le chiavi raccolte, incluso questo. Questa è l'azione predefinita.
Drop = ["ESC", "-", "TAB", "ENTER", "KPENTER", "LINEFEED..POWER"]
# Memorizza una mappa extra per queste chiavi, quando qualcuna è in stato "giù".
# Lo stato viene controllato tramite le condizioni "OFF:"|"ON:" nell'azione.
# (Inoltre, lo stato di queste chiavi deve persistere tra le cancellazioni del buffer.)
# ??? Come gestire i tasti CAPS e "LOCK" ???
StateKeys = ["L_CTRL", "L_SHIFT", "L_ALT", "L_META", "CAPS", "N_LOCK", "S_LOCK",
"R_CTRL", "R_SHIFT", "R_ALT", "R_META"]
# Test solo, ma non raccogliere.
# Ad esempio, uso F12 invece di BREAK su laptop scadenti con tastiere di bassa qualità (nuovi ThinkPad)
Test = ["F1..F10", "ZENKAKUHANKAKU", "102ND", "F11", "F12",
"RO..KPJPCOMMA", "SYSRQ", "SCALE", "HANGEUL..YEN",
"STOP..SCROLLDOWN", "NEW..MAX"]
Sono previsti un totale di 768 codici. (Ma per «ogni evenienza» ho inserito nel codice hswitcher una cattura per i «sorprese»).
All'interno ho dettagliato il riempimento dell'array con riferimenti alle funzioni «cosa fare». In golang questo (inaspettatamente) si è rivelato comodo e ovvio.
- «Drop» in questo punto prevedo di ridurlo al minimo. A favore di un'elaborazione più flessibile (mostrerò sotto).
3. Tabella con le classi delle finestre
# 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"
Le righe della tabella sono tra doppie parentesi quadre con il suo nome. Non è stato semplice farlo al primo colpo. A seconda della finestra attiva attualmente, è possibile selezionare le opzioni:
- Il proprio insieme di «tasti di scelta rapida» «Actions = …». Se non ci sono/presenti — non fare nulla.
- L'interruttore «MouseClickDrops» — cosa fare quando si rileva un clic del mouse. Poiché nel punto di accensione xswitcher non ci sono dettagli «dove cliccano», per impostazione predefinita resettiamo il buffer. Ma nei terminali (per esempio) potrebbe anche non essere necessario farlo. (di norma).
4. Una (o più) sequenze di pressione dei tasti attivano un hook piuttosto che un altro
# 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)" ]
Ho suddiviso gli hook in due tipi. Incorporati, con nomi «descrittivi» (NewWord, NewSentence, Compose) e programmabili.
I nomi di quelli programmabili iniziano con «Action.». Poiché TOML v1.4, i nomi con punti devono essere tra virgolette.
Sotto ognuno di essi deve essere descritta una sezione con lo stesso nome.
Per non far esplodere la mente delle persone con regex «nude» (per esperienza, scriverle può riuscire- a uno su dieci professionisti), implemento subito una sintassi aggiuntiva.
- «OFF:» (o «ON:») prima di regexp (espressione regolare) richiede che i pulsanti indicati siano rilasciati (o premuti).
Successivamente prevedo di fare un'espressione regolare «sleale». Con un controllo separato dei pezzi tra le pipe "|". Con l'obiettivo di ridurre il numero di registrazioni come "[LR]_SHIFT" (laddove non è chiaramente necessario). - «SEQ:» Se la condizione precedente è soddisfatta (o è assente), controlliamo rispetto all'espressione regolare «normale». Per maggiori dettagli rimando subito alla biblioteca «regexp». Perché io stesso non ho ancora trovato il tempo di chiarire il grado di compatibilità con i miei amati pcre («compatible con Perl»).
- L'espressione viene scritta sotto forma di «BOTTONE_1: CODICE1, BOTTONE_2: CODICE2» e così via, nell'ordine in cui arrivano i codici di scansione.
- Il controllo è sempre «applicato» alla fine della sequenza, quindi non è necessario aggiungere "$" alla fine.
- Tutti i controlli in una riga vengono eseguiti uno dopo l'altro e uniti tramite «E». Ma poiché il valore è descritto come un array, è possibile dopo la virgola scrivere un controllo alternativo. Se questo è necessario per qualche motivo.
- Valore «SeqLength = 8» limita la dimensione del buffer rispetto alla quale vengono eseguiti tutti i controlli. Poiché nella vita non ho (ad oggi) mai incontrato risorse infinite.
5. Assegnazione degli hook, descritti nella sezione precedente
# 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.
Il punto principale qui è— «Action = [Array]». Analogamente alla sezione precedente, c'è un insieme limitato di azioni integrate. E non c'è fondamentalmente alcun limite nella capacità di integrazione. (scrivere «Action.XXX» e non dimenticare di elaborare un'altra sezione per questo).
Includendo, la riesecuzione della parola in una disposizione corretta viene divisa in due parti: «cambia la disposizione come indicato lì» e «ripesca» («RetypeWord»).
Gli altri parametri vengono registrati in un «dizionario» («map» in golang) per questa azione, la loro lista dipende da ciò che è scritto in «Action».
Vari tipi di azioni possono essere descritti in un unico gruppo (sezioni). E possono essere separate. Come ho mostrato sopra.
Sto già impostando l'azione «Exec» — eseguire uno script esterno. Con l'opzione di inoltrargli nel stdin un buffer registrato.
- «Wait = 1» — attendere il completamento del processo avviato.
- Probabilmente, «a complemento» si vorrà impostare informazioni aggiuntive nell'ambiente, come il nome della classe della finestra da cui è stato catturato.
«Vuoi collegare il tuo gestore? Vai qui.»
Uff (ho respirato). Sembra che non abbia dimenticato nulla.
Ops! Ah, non ho dimenticato…
E la configurazione di avvio dov'è? Nel codice hard-coded? Circa così:
[ScanDevices]
# Deve esistere all'avvio. Autofusione in caso sia più giovane di 30 secondi
Test = "/dev/input/event0"
Respawn = 30
# Maschera di ricerca
Search = "/dev/input/event*"
# Nei miei thinkPads ci sono tali pseudo-tastiere che generano una miriade di eventi non necessari
Bypass = "(?i)Video|Camera" # "(?i)" ovviamente differisce dai classici pcre.
E dove ho dimenticato/sbagliato (senza questo—non si può fare), spero davvero che i lettori attenti non si tirino indietro dall'indicarmelo.
Buona fortuna!
Fonte: habr.com
