Dado que (el хswitcher en la etapa de «prueba de concepto») ha recibido bastante retroalimentación constructiva (lo cual es agradable), seguí dedicando mi tiempo libre al desarrollo del proyecto. Ahora quiero dedicar un poco de su tiempo... El segundo paso será un poco inusual: propuesta/discusión del diseño de la configuración.

De alguna manera, parece que para los programadores normales es muy aburrido configurar todos estos controles.
Para no ser vago, aquí hay un ejemplo de lo que estoy manejando.
En general, están bien pensados (y bastante bien implementados) Apache Kafka & ZooKeeper.
— ¿Configuración? ¡Pero eso es aburrido! Tapa-rapa xml (porque «de fábrica»).
— Oh, ¿y también quieren ACL? ¡Pero eso es tan complicado! Tapa-rapa... Así es como se siente.
En mi trabajo — justo al contrario. Correcto (lamentablemente, rara vez sale bien a la primera) el modelo construido permite luego fácilmente y sin complicaciones (bueno, casi) construir el esquema.
Recientemente vi un artículo en Habr sobre el arduo trabajo de los data scientists...
Resulta que este aspecto se implementa completamente en su caso. Y en mi práctica, como se dice, «versión ligera». Modelos voluminosos, programadores experimentados con OOP por delante, etc. — todo eso vendrá después, cuando/si despega. Y el constructor necesita empezar aquí y ahora con algo.
Volviendo al tema. Como base sintáctica elegí TOML .
Porque es (TOML) por un lado, editable por humanos. Y por otro lado, se traduce 1:1 a cualquiera de las sintaxis más comunes: XML, JSON, YAML.
Además, la implementación que utilicé de «github.com/BurntSushi/toml», aunque no es la más moderna (todavía en sintaxis 1.4), es sintácticamente compatible con el mismo («integrado») JSON.
Es decir, si se desea, se puede simplemente decir «vete al diablo con tu TOML, quiero XXX» y «parchear» el código de una sola línea.
Así que, si se desea escribir alguna ventana para configurar el хswitcher (definitivamente no yo) no se anticipan problemas con «tu maldito config».
Para todos los demás, la sintaxis basada en «clave = valor» (y literalmente un par de opciones un poco más complicadas, como = [algún, tipo, de, arreglo]) considero
intuitivamente conveniente.
Curiosamente, a mí mismo aproximadamente en la misma época (alrededor de 2013). Solo que, a diferencia de mí, el autor de TOML llegó con la debida amplitud.
Por lo tanto, ahora me resulta más fácil adaptar su implementación a mis necesidades, en lugar de lo contrario.
En general, tomamos TOML (muy parecido al antiguo INI de Windows). Y configuramos la configuración en la que describimos cómo enganchar una serie de ganchos dependiendo del conjunto de los últimos códigos de escaneo del teclado. A continuación, en partes, lo que he conseguido hasta ahora. Y las explicaciones de por qué he decidido esto.
0. Abstracciones básicas
- Designaciones de códigos de escaneo. Esto debe ser abordado, ya que los códigos numéricos por sí solos son completamente ilegibles para los humanos (esto lo digo en el huerto ).
Extraje «ecodes.go» de «golang-evdev» (no me tomé la molestia de buscar en la fuente original, aunque el autor lo menciona de manera bastante educada). Modifiqué un poco (por ahora) lo que era absolutamente terrible. Por ejemplo, «LEFTBRACE» → «L_BRACE». - Además, introduje el concepto de «teclas con estado». Dado que la gramática regular utilizada no se presta a largos pasajes. (Sin embargo, permite comprobar con un coste mínimo. Si solo se utiliza la grabación «directa»).
- Habrá un «deduplicador» incorporado para las pulsaciones. De este modo, el estado «repetir»=2 será registrado uno vez.
1. Sección de plantillas
[Templates] # "@name@" para simplificar expresiones
# Las palabras pueden constar de estos caracteres (regex)
"WORD" = "([0-9A-Z`;']|[LR]_BRACE|COMMA|DOT|SLASH|KP[0-9])"
De qué está compuesto una palabra en el lenguaje humano con transcripción fonética (qué diferente es de las grafemas aka «ideogramas»)? Какая-то ужасная «простыня». Поэтому сразу закладываю понятие «шаблона».
2. Qué hacer cuando algo es presionado (ha llegado otro código de escaneo)
[ActionKeys]
# Recoger clave y hacer la prueba para la secuencia de comandos
# !!! Los códigos repetidos (código=2) deben ser recogidos una vez por tecla!
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"]
# Descarta todas las teclas recopiladas, incluida esta. Esta es la acción por defecto.
Drop = ["ESC", "-", "TAB", "ENTER", "KPENTER", "LINEFEED..POWER"]
# Almacena un mapa adicional para estas teclas, cuando cualquiera está en estado "down".
# El estado se comprueba a través de las condiciones "OFF:"|"ON:" en la acción.
# (Además, el estado de estas teclas debe persistir entre las eliminaciones de búfer.)
# ??? ¿Cómo lidiar con las teclas CAPS y "LOCK" ???
StateKeys = ["L_CTRL", "L_SHIFT", "L_ALT", "L_META", "CAPS", "N_LOCK", "S_LOCK",
"R_CTRL", "R_SHIFT", "R_ALT", "R_META"]
# Solo prueba, pero no recopile.
# Por ejemplo, uso F12 en lugar de BREAK en laptops tontos con teclados malos (nuevos ThinkPads)
Test = ["F1..F10", "ZENKAKUHANKAKU", "102ND", "F11", "F12",
"RO..KPJPCOMMA", "SYSRQ", "SCALE", "HANGEUL..YEN",
"STOP..SCROLLDOWN", "NEW..MAX"]
En total se prevén 768 códigos. (Pero por si acaso, inserté en el código el hswitcher para atrapar los «sorpresas»).
Dentro, detallé el llenado de la matriz con enlaces a funciones de «qué hacer». En golang esto (inesperadamente) resultó ser conveniente y obvio.
- Planeo reducir "Drop" en este punto al mínimo. A favor de un procesamiento más flexible (veré abajo).
3. Tabla con las clases de ventanas
# 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"
Las filas de la tabla están en dobles corchetes cuadrados con su nombre. No resultó más fácil de inmediato. Dependiendo de la ventana activa actual, se pueden seleccionar opciones:
- Su propio conjunto de "teclas de acceso rápido" "Actions = …". Si está vacío — no hacer nada.
- Interruptor "MouseClickDrops" — qué hacer al detectar un clic del mouse. Dado que en el punto de activación xswitcher no hay detalles sobre "dónde hacen clic", por defecto se restablece el búfer. Pero en terminales (por ejemplo) esto podría no hacerse. (por lo general).
4. Una (o varias) secuencias de pulsaciones activan un hook u otro.
# 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)" ]
He dividido los hooks en dos tipos. Incorporados, con nombres "elocuentes" (NewWord, NewSentence, Compose) y programables.
Los nombres de los programables comienzan con "Action.". Desde TOML v1.4, los nombres con puntos deben estar entre comillas.
A continuación, cada uno debe tener su sección descrita con el mismo nombre.
Para no volarles la cabeza a las personas con regex "desnudos" (por experiencia, uno de cada diez escribir- puede que profesionales), inmediatamente introduzco una sintaxis adicional.
- "OFF:" (o "ON:") antes del regexp (expresión regular) requieren que las teclas especificadas sean soltadas (o presionadas).
Luego planeo hacer una expresión regular "deshonesta". Con una verificación separada de las piezas entre los pipes "|". Con el objetivo de reducir la cantidad de entradas del tipo "[LR]_SHIFT" (donde no es claramente necesario). - "SEQ:" Si la condición anterior se cumple (o está ausente), entonces se verifica en relación con el "regexp" normal. Para más detalles, remito a la biblioteca "regexp". Porque yo mismo aún no me he tomado la molestia de investigar el grado de compatibilidad con mis queridos pcre ("perl compatible").
- La expresión se escribe en forma de "BOTÓN_1: CÓDIGO1, BOTÓN_2: CÓDIGO2" etc., en el orden en que llegan los códigos escaneados.
- La verificación siempre se "acomoda" al final de la secuencia, por lo que no es necesario añadir "$" al final.
- Todas las verificaciones en una línea se realizan una tras otra y se combinan mediante "Y". Pero como el valor se describe en forma de array, se puede escribir una verificación alternativa después de la coma. Si es necesario.
- Valor "SeqLength = 8" limita el tamaño del búfer, respecto al cual se realizan todas las verificaciones. Dado que en la vida nunca me he encontrado (hasta ahora) con recursos infinitos.
5. Asignación de hooks, descritos en la sección anterior.
# 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.
Lo principal aquí es: «Action = [Array]». De manera similar a la sección anterior, hay un conjunto limitado de acciones integradas. Y también la posibilidad ilimitada de interconexión. (escribir «Action.XXX» y tomarse el tiempo de describir otra sección para ello).
Incluyendo, la reescritura de la palabra en la distribución corregida se divide en dos partes: «cambia la distribución como se indica allí» y «reescribe» («RetypeWord»).
Los demás parámetros se registran en el «diccionario» («map» en golang) para esta acción, su lista depende de lo escrito en «Action».
Se pueden describir varias acciones diferentes en un solo grupo (secciones). O se pueden desglosar. Como he mostrado arriba.
Defino inmediatamente la acción «Exec» - ejecutar un script externo. Con la opción de insertar en stdin un buffer grabado.
- «Wait = 1» - esperar la finalización del proceso iniciado.
- Probablemente, «para hacerlo completo» querrás establecer en el entorno información adicional como el nombre de la clase de la ventana desde la que se interceptó.
«¿Quieres conectar tu manejador? Ven aquí.»
Uf (suspiré). Parece que no he olvidado nada.
¡Ops! Ah, no olvidé...
¿Y la configuración de inicio dónde está? ¿En el hardcode? Aproximadamente así:
[ScanDevices]
# Debe existir al inicio. Respuesta automática en caso de que sea más joven que 30s
Test = "/dev/input/event0"
Respawn = 30
# Máscara de búsqueda
Search = "/dev/input/event*"
# En mis thinkPads hay tales pseudo-teclados con toneladas de eventos innecesarios
Bypass = "(?i)Video|Camera" # "(?i)" difiere claramente de los pcre 'clásicos'.
¿Y dónde olvidé/error? (sin esto, no hay forma), tengo mucha esperanza de que los lectores atentos no duden en señalar.
¡Buena suerte!
Fuente: habr.com
