Layout-Korrektor «hswitcher» für Linux: Schritt zwei

Da das vorheriger Beitrag (der Switcher in der Phase „Proof of Concept“) erhielt ziemlich viele konstruktive Rückmeldungen (was schön ist), habe ich weiterhin meine Freizeit mit der Entwicklung des Projekts verbracht. Jetzt möchte ich ein wenig von Ihrer Zeit investieren... Der zweite Schritt wird etwas ungewohnt sein: Vorschlag/Diskussion des Konfigurationsdesigns.

Layout-Korrektor «hswitcher» für Linux: Schritt zwei

Es ergibt sich irgendwie so, dass es normalen Programmierern ziemlich langweilig ist, all diese Drehknöpfe einzurichten.

Um nicht unkonkret zu sein, hier ist ein Beispiel aus dem, mit dem ich zu tun habe.
Klar gut durchdachte (und nicht schlecht umgesetzte) Apache Kafka & ZooKeeper.
— Konfiguration? Aber das ist doch langweilig! Schnell-schnell XML (weil „out of the box“).
— Oh, und Sie wollen auch ACL? Aber das ist so mühsam! Schnell-schnell… So in etwa.

In meiner Arbeit ist es genau das Gegenteil. Richtig (leider gibt es das fast nie beim ersten Mal) ermöglicht das erstellte Modell ein leichtes und müheloses (naja, fast) Erstellen des Schemas.

Kürzlich bin ich auf Habré auf einen Artikel über die schwierige Arbeit von Data Scientists gestoßen...
Offensichtlich wird dieser Aspekt bei ihnen vollständig umgesetzt. In meiner Praxis, wie man so sagt, ist es eine „Light-Version“. Mehrteilige Modelle, versierte Programmierer mit OOP im Schlepptau usw. – das kommt später, wenn/es abhebt. Aber der Konstrukteur muss hier und jetzt einfach irgendwo anfangen.

Kommen wir zur Sache. Als syntaktische Grundlage habe ich TOML gewählt von diesem Bürger.

Weil er (TOML) einerseits menschenlesbar ist. Andererseits wird er 1:1 in jeden der verbreiteten Syntaxe: XML, JSON, YAML übersetzt.
Darüber hinaus ist die von mir verwendete Implementierung von „github.com/BurntSushi/toml“, obwohl nicht die aktuellste (immer noch Syntax 1.4), syntaktisch kompatibel mit dem gleichen („eingebetteten“) JSON.

Das heißt, wenn man möchte, kann man einfach sagen „geh weg mit deinem TOML, ich will XXX“ und den Code mit nur einer Zeile „patchen“.

Somit wird, wenn gewünscht, für die Konfiguration des Switchers irgendwelche Fenster zu schreiben (definitiv nicht ich) die Probleme „mit deiner verdammten Konfiguration“ – sind nicht abzusehen.

Für alle anderen ist die Syntax auf Basis von „Schlüssel = Wert“ (und buchstäblich ein paar kompliziertere Optionen, wie = [einige, Arrays]) halte ich
intuitiv sinnvoll.
Was interessant ist, bei mir „hat es gebrannt“ ungefähr zur gleichen Zeit (um 2013). Nur, im Gegensatz zu mir, ist der Autor von TOML mit entsprechendem Elan gestartet.

Deshalb ist es für mich jetzt einfacher, seine Implementierung an mich anzupassen, als umgekehrt.

Insgesamt nehmen wir TOML (sehr ähnlich dem alten Windows INI). Und definieren die Konfiguration, in der wir beschreiben, wie eine Reihe von Hooks abhängig von der letzten Tastenanschlagsequenz angehängt werden können. Im Folgenden die Teile, die bisher entstanden sind, und Erklärungen, warum ich das so entschieden habe.

0. Grundlegende Abstraktionen

  • Bezeichnungen der Tastenanschläge. Damit muss unbedingt etwas geschehen, da einfach nur numerische Codes absolut nicht lesbar sind (das meine ich im übertragenen Sinne). loloswitcher).
    Ich habe "ecodes.go" aus "golang-evdev" extrahiert (ich war zu faul, um in die Quelle zu schauen, obwohl sie beim Autor durchaus anständig aufgeführt ist). Ich habe ein bisschen (bis jetzt) das Schrecklichste überarbeitet. So etwas wie „LEFTBRACE“ → „L_BRACE“.
  • Zusätzlich habe ich das Konzept der "Tasten mit Zustand" eingeführt. Da die verwendete reguläre Grammatik keine langen Phrasen zulässt. (Aber es erlaubt, mit minimalen Auflagen zu überprüfen. Wenn man nur die "direkte" Aufzeichnung nutzt.)
  • Es wird einen eingebauten "Deduplicator" für Tastendrücke geben. Somit wird der Zustand "Wiederholung"=2 festgehalten. eine Mal heruntergeladen.

1. Vorlagenbereich

[Templates] # "@name@" zur Vereinfachung von Ausdrücken
 # Wörter können aus diesen Zeichen bestehen (Regex)
 "WORD" = "([0-9A-Z`;']|[LR]_BRACE|COMMA|DOT|SLASH|KP[0-9])"

Woraus ein Wort der menschlichen Sprache mit phonetischer Aufzeichnung besteht (Verglichen mit Graphemen aka „Hieroglyphen“)? Какая-то ужасная «простыня». Поэтому сразу закладываю понятие «шаблона».

2. Was tun, wenn etwas gedrückt wird (ein neuer Tastenanschlag ist gekommen)

[ActionKeys]
 # Erfasse die Taste und teste die Befehlssequenz
 # !!! Wiederholungscodes (code=2) müssen für jede Taste einmal gesammelt werden!
 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"]

 # Alle gesammelten Tasten, einschließlich dieser, ablegen. Das ist die Standardaktion.
 Drop = ["ESC", "-", "TAB", "ENTER", "KPENTER", "LINEFEED..POWER"]
 # Zusätzliche Karte für diese Tasten speichern, wenn eine im "down"-Zustand ist.
 # Der Zustand wird über die Bedingungen "OFF:"|"ON:" in der Aktion überprüft.
 # (Außerdem muss der Zustand dieser Tasten zwischen den Pufferspeicher-Abläufen bestehen bleiben.)
 # ??? Wie mit CAPS und "LOCK"-Tasten umgehen ???
 StateKeys = ["L_CTRL", "L_SHIFT", "L_ALT", "L_META", "CAPS", "N_LOCK", "S_LOCK",
              "R_CTRL", "R_SHIFT", "R_ALT", "R_META"]

 # Nur testen, aber nicht sammeln.
 # Zum Beispiel benutze ich F12 anstelle von BREAK auf dummen Laptops mit schrecklichen Tastaturen (neue ThinkPads)
 Test = ["F1..F10", "ZENKAKUHANKAKU", "102ND", "F11", "F12",
          "RO..KPJPCOMMA", "SYSRQ", "SCALE", "HANGEUL..YEN",
          "STOP..SCROLLDOWN", "NEW..MAX"]

Insgesamt sind 768 Codes vorgesehen. (Aber „vorsichtshalber“ habe ich im Code eine xswitcher-Behandlung für „Überraschungen“ eingefügt).
Innerhalb habe ich das Ausfüllen des Arrays mit Links zu Funktionen "was zu tun ist" detailliert beschrieben. In Golang ist das (überraschenderweise) sich als praktisch und offensichtlich herausgestellt.

  • Ich plane, „Drop“ an dieser Stelle auf ein Minimum zu reduzieren. Zugunsten einer flexibleren Verarbeitung (wie ich weiter unten zeigen werde).

3. Tabelle mit Fensternklassen

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

Die Zeilen der Tabelle sind in doppelten eckigen Klammern mit ihrem Namen. Es war nicht einfach, sofort eine Lösung zu finden. Je nach dem aktuell aktiven Fenster können Optionen ausgewählt werden:

  • Mein eigener Satz von „Hotkeys“ „Actions = …“. Wenn nicht vorhanden/leer – nichts tun.
  • Der Schalter „MouseClickDrops“ – was tun, wenn ein Mausklick erkannt wird. Da es am Punkt der Aktivierung xswitcher keine Details „wohin da geklickt wird“ gibt, setzen wir standardmäßig den Puffer zurück. Aber in Terminals (zum Beispiel) könnte man das auch lassen. (in der Regel).

4. Eine (oder mehrere) Tastenkombinationen lösen den jeweiligen Hook aus.

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

Ich habe die Hooks in zwei Typen unterteilt. Eingebaute mit „sprechenden“ Namen (NewWord, NewSentence, Compose) und programmierbare.

Die Namen der programmierbaren beginnen mit „Action.“. Da TOML v1.4, müssen Namen mit Punkten in Anführungszeichen stehen.

Darunter sollte für jede ein Abschnitt mit dem gleichen Namen beschrieben werden. um den Kopf der Menschen nicht mit „nackten“ regulären Ausdrücken zu verwirren (aus Erfahrung, deren.

zu schreiben - könnte einer von zehnProfis ), führe ich sofort zusätzliche Syntax ein.„OFF:“ (oder „ON:“)

  • vor regexp (regulärem Ausdruck) verlangen, dass die angegebenen Tasten vorher losgelassen (oder gedrückt) werden. Als nächstes möchte ich einen „unfairen“ regulären Ausdruck erstellen. Mit separater Überprüfung der Teile zwischen den Pipes "|". Ziel ist es, die Anzahl der Einträge wie "[LR]_SHIFT" zu reduzieren (wo es eindeutig nicht nötig ist).
    „SEQ:“
  • Wenn die vorherige Bedingung erfüllt (oder nicht vorhanden) ist, überprüfen wir basierend auf dem „gewöhnlichen“ regulären Ausdruck. Für Details verweise ich sofort auf die Bibliothek „regexp“. Weil ich selbst mir bisher nicht die Mühe gemacht habe, die Kompatibilität mit meinen Lieblings-pcre („perl-kompatibel“) zu ermitteln. Der Ausdruck wird in der Form
  • „BUTTON_1: CODE1, BUTTON_2: CODE2“ usw., in der Reihenfolge des Eingangs der Scankodes. Die Überprüfung wird immer am Ende der Sequenz angehängt
  • , daher muss "$“ am Ende nicht hinzugefügt werden.Alle Überprüfungen in einer Zeile werden nacheinander ausgeführt
  • und durch „UND“ verbunden. Da der Wert jedoch als Array beschrieben ist, kann nach dem Komma eine alternative Überprüfung angegeben werden. Wenn das aus irgendeinem Grund notwendig ist. „SeqLength = 8“
  • Wert begrenzt die Größe des Puffers, in Bezug auf den alle Überprüfungen durchgeführt werden. beschränkt die Puffergröße, für die alle Prüfungen durchgeführt werden. Da ich in meinem Leben (bis jetzt) keine unbegrenzten Ressourcen getroffen habe.

5. Zuweisung der Hooks, die im vorherigen Abschnitt beschrieben wurden

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

Das Wichtigste hier ist — „Action = [Array]“. Ähnlich wie im vorherigen Abschnitt gibt es eine begrenzte Anzahl von eingebauten Aktionen. Und prinzipiell keine Einschränkung hinsichtlich der Verbindungsmöglichkeiten (schreibe „Action.XXX“ und scheue dich nicht, einen weiteren Abschnitt dafür zu erstellen).
Darunter wird die Neusetzung des Textes in der korrigierten Anordnung in zwei Teile getrennt: „Ändere die Anordnung wie dort angegeben“ und „Neutippen“ („RetypeWord“).

Die übrigen Parameter werden in ein „Wörterbuch“ geschrieben („map“ in golang) für diese Aktion, ihre Liste hängt von dem ab, was in „Action“ geschrieben steht.

Mehrere verschiedene Aktionen können in einem Block beschrieben werden (Abschnitte). Man kann sie auch auseinanderziehen. Wie ich oben gezeigt habe.

Ich lege sofort die Aktion „Exec“ fest — ein externes Skript ausführen. Mit der Option, den geschriebenen Puffer in stdin zu stopfen.

  • „Wait = 1“ — auf den Abschluss des gestarteten Prozesses warten.
  • Wahrscheinlich möchte man „nebenbei“ zusätzliche Informationen wie den Namen der Fensterklasse, von der aus abgefangen wurde, in die Umgebung setzen.
    „Möchten Sie Ihren Handler anschließen? Hierher.“

Uff (seufz). Scheint, als hätte ich nichts vergessen.

Oops! Ah, habe ich nicht vergessen…
Und wo ist die Startkonfiguration? Im Hardcode? Ungefähr so:

[ScanDevices]
 # Muss beim Start vorhanden sein. Selber neu starten, wenn es jünger als 30s ist
 Test = "\/dev\/input\/event0"
 Respawn = 30
 # Suchmaske
 Search = "\/dev\/input\/event*"
 # In meinen ThinkPads gibt es solche Pseudo-Tastaturen mit vielen unnötigen Ereignissen
 Bypass = "(?i)Video|Camera" # „(?i)“ unterscheidet sich offensichtlich von „klassischen“ PCREs.

Und wo habe ich vergessen/irrtümlich (ohne das geht es nicht), hoffe ich sehr, dass die aufmerksamen Leser nicht zögern werden, mit dem Finger darauf zu zeigen.

Viel Glück!

Quelle: habr.com

60GB SSD 8Gb DDR4