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

In qualche modo, ai programmatori normali risulta estremamente noioso configurare tutti questi controlli.
Per non essere poco credibile, ecco un esempio di ciò con cui ho a che fare.
Ottimamente concepiti (e ben realizzati) Apache Kafka & ZooKeeper.
— Configurazione? Ma è così noioso! Tappa e avvia xml (perché «pronto all'uso»).
— Oh, e volete anche gli ACL? Ma è così laborioso! Tappa e avvia… Così.
Nella mia esperienza è esattamente l'opposto. Correttamente (purtroppo, quasi mai al primo colpo) il modello costruito permette di raccogliere facilmente e senza sforzo (beh, quasi) lo schema.
Ho trovato recentemente un articolo su Habr riguardo il lavoro difficile dei data scientist…
A quanto pare, questo aspetto è completamente implementato da loro. Nella mia esperienza, come si suol dire, è una 'versione leggera'. Modelli multivolume, programmatori esperti armati di OOP, ecc. — tutto questo arriverà poi, quando/if decollerà. Ma il costruttore ha bisogno di iniziare da qualcosa, qui e ora.
Passando al sodo. Come base sintattica, ho scelto TOML .
Perché lui (TOML) da un lato è umano-editabile. Dall'altro si traduce 1:1 in qualsiasi sintassi più comune: XML, JSON, YAML.
Inoltre, l'implementazione che ho usato da 'github.com/BurntSushi/toml' anche se non è la più trendy (ancora sintassi 1.4), è compatibile sintatticamente con lo stesso JSON 'integrato'.
Cioè, se lo si desidera, si può semplicemente dire 'vai a quel paese con il tuo TOML, voglio XXX' e 'patchare' il codice con una sola riga.
Quindi, se si desidera scrivere per configurare hswitcher qualche finestra (non certo io) non si prevedono problemi 'con questa dannata configurazione vostra'.
Per tutti gli altri, la sintassi basata su 'chiave = valore' (e letteralmente qualche opzione un po' più complessa, tipo = [qualche, array]) la considero
intuitivamente comoda.
Curiosamente, proprio circa nello stesso periodo (intorno al 2013). Solo che, a differenza di me, l'autore di TOML è entrato in scena con un certo slancio.
Pertanto, ora è più facile adattare la sua implementazione alle mie esigenze, piuttosto che il contrario.
In generale, prendiamo TOML (molto simile al vecchio INI di Windows). E impostiamo la configurazione in cui descriviamo come collegare una serie di hook in base all'insieme degli ultimi codici scan dalla tastiera. Di seguito, a pezzi, ciò che ho ottenuto finora. E spiegazioni sul perché ho preso queste decisioni.
0. Astrazioni di base
- Designazioni dei codici scan. È fondamentale fare qualcosa al riguardo, poiché codici numerici puri non sono affatto leggibili per gli esseri umani (questo lo dico come metafora ).
Ho estratto "ecodes.go" da "golang-evdev" (non ho avuto voglia di andare alla fonte, anche se l'autore lo indica in modo piuttosto cortese). Ho fatto alcune piccole correzioni (per ora) a cose spaventose. Tipo "LEFTBRACE" → "L_BRACE". - Ho anche introdotto il concetto di "tasti con stato". Poiché la grammatica regolare utilizzata non consente lunghi passaggi. (Ma permette di controllare con costi minimi. Se si utilizza solo la registrazione "diretta".)
- Sarà integrato un "deduplicatore" per i tasti premuti. In questo modo, lo stato "ripetizione" = 2 verrà registrato uno volta.
1. Sezione 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 è composto un termine del linguaggio umano con trascrizione fonetica (a differenza delle grafemi, aka «ideogrammi»)? Какая-то ужасная «простыня». Поэтому сразу закладываю понятие «шаблона».
2. Cosa fare quando si preme un tasto (è arrivato un altro codice di scansione)
[ActionKeys]
# Raccogli il tasto e testa la sequenza di comandi
# !!! I codici di ripetizione (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"]
# Elimina tutti i tasti raccolti, compreso questo. Questa è l'azione predefinita.
Drop = ["ESC", "-", "TAB", "ENTER", "KPENTER", "LINEFEED..POWER"]
# Memorizza una mappa extra per questi tasti, quando uno di essi è in stato "giù".
# Lo stato è verificato tramite condizioni "OFF:"|"ON:" nell'azione.
# (Inoltre, lo stato di questi tasti deve persistere tra le eliminazioni 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"]
# Solo per test, ma non raccogliere.
# Ad esempio, utilizzo F12 invece di BREAK su laptop scadenti con tastiere pessime (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 "paranoia" ho inserito nel codice un hswitcher per intercettare "sorprese").
All'interno ho dettagliato il riempimento di un array con link alle funzioni 'cosa fare'. In golang è risultata (inaspettatamente) comoda e ovvia.
- Piano di accorciare 'Drop' al minimo in questo punto, a favore di una gestione 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 racchiuse tra doppie parentesi quadre con il suo nome. Non è stato facile da subito. In base alla finestra attiva corrente, è possibile selezionare le opzioni:
- Il proprio insieme di 'scorciatoie da tastiera' 'Actions = ...'. Se non ci sono / è vuoto, non fare nulla.
- Interruttore 'MouseClickDrops' – cosa fare quando viene rilecato un clic del mouse. Poiché nel punto di attivazione xswitcher non ci sono dettagli su 'dove si clicca', per default si resetta il buffer. Ma nei terminali (ad esempio) si può anche non farlo (di norma).
4. Una (o più) sequenze di tasti attivano un determinato 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)" ]
Ho suddiviso gli hook in due tipi. Integrati, con nomi 'parlanti' (NewWord, NewSentence, Compose) e programmabili.
I nomi dei programmabili iniziano con 'Action.'. Dal momento che TOML v1.4, i nomi con punti devono essere tra virgolette.
Qui sotto per ciascuno deve essere descritto un paragrafo con lo stesso nome.
Per evitare di confondere le persone con regex "nude" (per esperienza, il loro scrivere-ci può essere uno su dieci professionisti), implemento subito una sintassi aggiuntiva.
- «OFF:» (o «ON:») prima dell'espressione regexp (espresso regolare) richiede che i pulsanti successivi siano rilasciati (o premuti).
Successivamente, intendo creare un'espressione regolare "sleale". Con controlli separati delle parti tra le pipe "|". Per ridurre il numero di registrazioni come "[LR]_SHIFT" (laddove non è chiaramente necessario). - «SEQ:» Se la condizione precedente è soddisfatta (o assente), poi controlliamo rispetto all'espressione regolare "normale". Per ulteriori dettagli, rinvio immediatamente alla libreria «regexp». Perché io stesso non mi sono ancora preso la briga di scoprire il grado di compatibilità con i miei amati pcre ("perl compatible").
- L'espressione è scritta come «BOTTONE_1: CODICE1, BOTTONE_2: CODICE2» e così via, in ordine di ricezione dei codici scansionati.
- Il controllo viene sempre "accoppiato" alla fine della sequenza, quindi non è necessario aggiungere "$" alla fine.
- Tutti i controlli in una riga vengono eseguiti uno dopo l'altro. e sono uniti per "I". Ma poiché il valore è descritto come un array, dopo la virgola è possibile scrivere un controllo alternativo. Se questo è necessario per qualche motivo.
- Significato "SeqLength = 8" limita la dimensione del buffer, rispetto alla quale vengono eseguiti tutti i controlli. Poiché nella vita non mi sono mai imbattuto (fino ad oggi) in risorse infinite.
5. Assegnazione dei 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.
La cosa principale qui è - "Action = [Array]". Analogamente alla sezione precedente, c'è un insieme limitato di azioni integrate. E una possibilità non limitata di integrazione (scrivere "Action.XXX" e non dimenticarsi di scrivere un'altra sezione per esso).
Incluso, la riscrittura della parola nella disposizione corretta è divisa in due parti: "cambia la disposizione come indicato lì" e "riscrivi" ("RetypeWord").
Gli altri parametri sono registrati in un "dizionario" ("map" in golang) per questa azione, la loro lista dipende da ciò che è scritto in "Action".
Diverse azioni possono essere descritte in un unico insieme (sezione). Ma possono anche essere distinte. Come ho mostrato sopra.
Immediatamente includo l'azione "Exec" - eseguire uno script esterno. Con l'opzione di inviare il buffer registrato a stdin.
- «Wait = 1» — attendere il completamento del processo avviato.
- Probabilmente, vorrete aggiungere informazioni extra all'ambiente, come il nome della classe della finestra da cui è stata intercettata.
«Volete collegare il vostro gestore? Ecco dove.»
Uff (sospira). Sembra che non abbia dimenticato nulla.
Ops! Ah, non ho dimenticato...
E dove si trova la configurazione di avvio? Nel codice hard-coded? Circa così:
[ScanDevices]
# Deve esistere all'avvio. Riavviarsi in caso sia più giovane di 30s
Test = "/dev/input/event0"
Respawn = 30
# Maschera di ricerca
Search = "/dev/input/event*"
# Nei miei ThinkPad ci sono tali pseudo-tastiere con tonnellate 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 facciano scrupoli a farlo notare.
Buona fortuna!
Fonte: habr.com
