Corretor de layouts “xswitcher” para Linux: passo dois

Uma vez que publicação anterior (o xswitcher na fase de «prova de conceito») recebeu muitos feedbacks construtivos (o que é bom), continuei gastando meu tempo livre no desenvolvimento do projeto. Agora quero gastar um pouco do seu… O segundo passo será um pouco incomum: proposta/discussão do design da configuração.

Corretor de layouts “xswitcher” para Linux: passo dois

É curioso que programadores normais acham configurar todas essas opções extremamente chato.

Para não ser vago, aqui está um exemplo do que estou lidando.
Excelentemente concebido (e bem implementado) Apache Kafka & ZooKeeper.
— Configuração? Mas isso é chato! Um simples xml (porque «fora da caixa»).
— Oh, e você ainda quer ACL? Mas isso é tão tedioso! Um simples… Algo assim.

E no meu trabalho é exatamente o oposto. Correto (infelizmente, raramente funciona na primeira tentativa) o modelo construído permite então facilmente e sem estresse (bem, quase) montar o esquema.

Recentemente me deparei com um artigo no Habr sobre o trabalho difícil dos data scientists…
Parece que esse aspecto é completamente abordado por eles. E na minha prática, como se diz, «versão leve». Modelos complexos, programadores experientes com OOP à disposição, etc. — isso tudo aparecerá depois, quando/se decolar. O construtor precisa começar algo aqui e agora.

Chegando ao ponto. Como base sintática, escolhi TOML deste cidadão.

Porque ele (TOML) por um lado é editável por humanos. E por outro lado — é traduzido 1:1 em qualquer uma das sintaxes mais comuns: XML, JSON, YAML.
Além disso, a implementação que usei do «github.com/BurntSushi/toml», embora não seja a mais moderna (ainda usa a sintaxe 1.4), é sintaticamente compatível com o mesmo JSON («embarcado»).

Ou seja, se desejado, pode-se simplesmente dizer «vá se danar com esse seu TOML, eu quero XXX» e «fazer um patch» no código com uma única linha.

Assim, se desejado escrever algumas janelas para configurar o xswitcher (com certeza não sou eu) não haverá problemas «com esse seu maldito config».

Para todos os outros, a sintaxe baseada em «chave = valor» (e literalmente algumas opções mais complicadas, tipo = [algum, array]) considero
intuitivamente conveniente.
Curiosamente, o autor «ficou irritado» aproximadamente na mesma época (por volta de 2013). Apenas, ao contrário de mim, o autor do TOML entrou com a devida grandeza.

Portanto, agora é mais fácil para mim adaptar sua implementação para mim, e não o contrário.

Então, vamos usar TOML (muito parecido com o antigo INI do Windows). E configurar a configuração, na qual descrevemos como anexar uma série de hooks dependendo do conjunto dos últimos códigos scancodes do teclado. Abaixo estão os pedaços - o que consegui até o momento. E explicações sobre porque decidi assim.

0. Abstrações Básicas

  • Designações de scancodes. Precisamos fazer algo a respeito, pois códigos numéricos simples não são legíveis para humanos (como eu disse antes) loloswitcher).
    Extraí "ecodes.go" de "golang-evdev" (desistindo de vasculhar a fonte original, embora o autor tenha mencionado isso de forma bastante educada). Fiz algumas correções (por enquanto) para tornar as coisas menos atrozes. Tipo "LEFTBRACE" → "L_BRACE".
  • Além disso, introduzi o conceito de "teclas com estado". Como a gramática regular utilizada não permite passagens longas. (Mas permite verificar com mínima sobrecarga. Se usar apenas a gravação "direta").
  • Haverá um "deduplicador" interno de teclas pressionadas. Assim, o estado "repetir"=2 será registrado. uma vez.

1. Seção de Templates

[Templates] # "@name@" para simplificar expressões
 # As palavras podem consistir nesses caracteres (regex)
 "WORD" = "([0-9A-Z`;']|[LR]_BRACE|COMMA|DOT|SLASH|KP[0-9])"

Do que é feita uma palavra da língua humana com a transcrição fonética. (diferente das grafemas, também conhecidos como "ideogramas")? Какая-то ужасная «простыня». Поэтому сразу закладываю понятие «шаблона».

2. O que fazer quando algo é pressionado (recebeu um novo código scancode)

[ActionKeys]
 # Coletar a chave e fazer o teste para sequência de comando
 # !!! Códigos repetidos (code=2) devem ser coletados uma vez por chave!
 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"]

 # Descartar todas as chaves coletadas, incluindo esta. Esta é a ação padrão.
 Drop = ["ESC", "-", "TAB", "ENTER", "KPENTER", "LINEFEED..POWER"]
 # Armazenar um mapa extra para estas teclas, quando qualquer uma está no estado "baixo".
 # O estado é verificado através das condições "OFF:"|"ON:" na ação.
 # (Além disso, o estado destas teclas deve persistir entre descartes de buffer.)
 # ??? Como lidar com as teclas 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"]

 # Teste apenas, mas não colete.
 # Ex. Eu uso F12 em vez de BREAK em laptops ruins com teclados ruins (novos ThinkPads)
 Test = ["F1..F10", "ZENKAKUHANKAKU", "102ND", "F11", "F12",
          "RO..KPJPCOMMA", "SYSRQ", "SCALE", "HANGEUL..YEN",
          "STOP..SCROLLDOWN", "NEW..MAX"]

Um total de 768 códigos foram previstos. (Mas para "caso necessário", adicionei no código do xswitcher uma captura de "surpresas").
Dentro, descrevi o preenchimento do array com links para funções "o que fazer". No golang isso (surpreendentemente) acabou sendo conveniente e óbvio.

  • Planejo reduzir "Drop" a um mínimo neste ponto. Em favor de um processamento mais flexível (mostrarei abaixo).

3. Tabela com classes de janelas

# 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"

Linhas da tabela — entre colchetes duplos com o seu título. Não consegui fazer mais simples de imediato. Dependendo da janela ativa no momento, é possível escolher opções:

  • Seu próprio conjunto de "atalhos" "Actions = …". Se estiver vazio — não fazer nada.
  • Interruptor "MouseClickDrops" — o que fazer ao detectar um clique do mouse. Como no ponto de ativação xswitcher não há detalhes sobre "onde exatamente clica", por padrão, zeramos o buffer. Mas em terminais (por exemplo) isso pode não ser necessário. (geralmente).

4. Uma (ou várias) sequências de pressionamentos ativam o respectivo 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)" ]

Dividi os hooks em dois tipos. Incorporados, com nomes "falantes" (NewWord, NewSentence, Compose) e programáveis.

Nomes programáveis começam com "Action.". Como na versão 1.4 do TOML, nomes com pontos devem estar entre aspas.

Abaixo, para cada um deve ser descrito um trecho com o mesmo nome.

Para não confundir as pessoas com regex "nuas" (por experiência, elas escrever- podem ser feitas por um em cada dez profissionais), já implanto uma sintaxe adicional.

  • "OFF:" (ou "ON:") antes do regex (expressão regular) exige que as teclas especificadas a seguir estejam liberadas (ou pressionadas).
    A partir daí, pretendo fazer uma expressão regular "desonesta". Com verificação separada de partes entre os pipes "|". Com o objetivo de reduzir o número de entradas do tipo "[LR]_SHIFT" (onde isso não é realmente necessário).
  • "SEQ:" Se a condição anterior for satisfeita (ou estiver ausente), checaremos em relação à expressões regulares "normais". Para mais detalhes, encaminho diretamente para a biblioteca "regexp". Porque eu mesmo ainda não tomei a iniciativa de descobrir o nível de compatibilidade com meus queridos pcre ("perl compatible").
  • A expressão é escrita no formato "BUTTON_1: CODE1, BUTTON_2: CODE2" etc., na ordem em que os códigos dos botões são recebidos.
  • A verificação sempre "se encaixa" no final da sequência, portanto, não é necessário adicionar "$" ao final.
  • Todas as verificações em uma linha são executadas uma após a outra e combinadas por "E". Mas como o valor é descrito como um array, pode-se escrever uma verificação alternativa após a vírgula. Se isso for necessário por algum motivo.
  • O valor "SeqLength = 8" limita o tamanho do buffer, em relação ao qual todas as verificações são executadas. Como na vida eu (até agora) nunca encontrei recursos infinitos.

5. Atribuição de hooks, descritos na seção 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.

O principal aqui é — «Action = [Array]». Da mesma forma que na seção anterior, há um conjunto limitado de ações integradas. E a possibilidade ilimitada de combinação. (escreva «Action.XXX» e não se esqueça de descrever mais uma seção para isso).
Incluindo, a reentrada da palavra na configuração corrigida se divide em duas partes: «troque a configuração como está lá especificado» e «reentrada» («RetypeWord»).

Os outros parâmetros são gravados em um «dicionário» («map» no golang) para esta ação, sua lista depende do que foi escrito em «Action».

Várias ações diferentes podem ser descritas em um único bloco (seções). E pode ser dividido. Como mostrei acima.

Imediatamente estabeleço a ação «Exec» — executar um script externo. Com a opção de empurrar para ele um buffer gravado em stdin.

  • «Wait = 1» — aguardar a conclusão do processo iniciado.
  • Provavelmente, para adicionar mais, você vai querer expor informações adicionais ao ambiente, como o nome da classe da janela de onde foi capturado.
    «Quer conectar seu manipulador? Você deve ir aqui.»

Ufa (respirei). Parece que não esqueci nada.

Ops! Ah, não esqueci…
E onde está a configuração de inicialização? No código fixo? Mais ou menos assim:

[ScanDevices]
 # Deve existir ao iniciar. Auto-recriar caso seja mais jovem que 30s
 Test = "\/dev\/input\/event0"
 Respawn = 30
 # Máscara de busca
 Search = "\/dev\/input\/event*"
 # Nos meus thinkPads há tais pseudo-teclados com toneladas de eventos desnecessários
 Bypass = "(?i)Vídeo|Câmera" # "(?i)" obviamente difere do clássico pcre.

E onde eu esqueci/errei (sem isso — não dá), espero muito que leitores atentos não se esqueçam de apontar.

Boa sorte!

Fonte: habr.com

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster