Da (der "Switcher" befindet sich in der Phase "Proof of Concept") hat viele konstruktive Rückmeldungen erhalten (was angenehm ist), ich habe weiterhin meine Freizeit mit der Entwicklung des Projekts verbracht. Jetzt möchte ich ein wenig von Ihrer Zeit in Anspruch nehmen… Der zweite Schritt wird nicht ganz gewöhnlich sein: Vorschlag/Diskussion über das Design der Konfiguration.

Es scheint so zu sein, dass es normalen Programmierern total langweilig ist, all diese Regler einzustellen.
Um nicht unüberlegt zu sein, hier ist ein Beispiel von dem, womit ich arbeite.
Im Großen und Ganzen gut durchdacht (und recht gut umgesetzt) Apache Kafka & ZooKeeper.
— Konfiguration? Das ist doch langweilig! Schnell-schnell XML (weil es "out of the box" ist).
— Oh, wollen Sie auch ACLs? Aber das ist so mühsam! Schnell-schnell… So in etwa.
Und in meiner Arbeit ist es genau andersherum. Richtig (leider klappt das beim ersten Mal fast nie) ermöglicht das Modell, weiter leicht und unbeschwert (naja, fast) ein Schema zu erstellen.
Ich bin neulich auf Habré auf einen Artikel über die beschwerliche Arbeit von Data Scientists gestoßen…
Es stellt sich heraus, dass dieser Punkt bei ihnen vollständig umgesetzt wird. In meiner Praxis, sozusagen eine 'Light-Version'. Mehrteilige Modelle, erfahrene Programmierer mit OOP im Schlepptau usw. — das kommt alles später, wenn / falls es kickt. Der Konstrukteur muss hier und jetzt irgendwo anfangen.
Kommen wir zur Sache. Als syntaktische Grundlage habe ich TOML gewählt .
Weil er (TOML) einerseits menschenlesbar ist. Andererseits wird er 1:1 in jede der gebräuchlicheren Syntaxen übertragen: XML, JSON, YAML.
Darüber hinaus ist die von mir verwendete Implementierung von 'github.com/BurntSushi/toml', obwohl sie nicht die modernste (immer noch Syntax 1.4) ist, syntaktisch kompatibel mit dem gleichen ('eingebetteten') JSON.
Das heißt, wenn man möchte, kann man einfach sagen 'Verschwinde mit deinem TOML, ich will XXX' und den Code mit nur einer Zeile patchen.
So kann man bei Bedarf einige Fenster für die Konfiguration von hswitcher schreiben (Bestimmt nicht ich) Probleme mit 'diesem verdammten Konfigurationsfile' sind nicht zu erwarten.
Für alle anderen Syntaxe basierend auf 'Schlüssel = Wert' (und mit buchstäblich ein paar komplizierteren Optionen, wie = [irgendeines, Arrays]) halbe ich
als intuitiv handlich.
Interessanterweise hat der Autor TOML etwa zur gleichen Zeit (um 2013) "Abschied" genommen, aber im Gegensatz zu mir hat er es mit einem gewissen Pomp getan. Deshalb ist es jetzt einfacher für mich, seine Implementierung anzupassen, als umgekehrt.
Kurz gesagt, wir nehmen TOML (sehr ähnlich wie die alte Windows INI). Und wir erstellen eine Konfiguration, in der wir beschreiben, wie man eine Reihe von Hooks abhängig von den letzten Scankodencodes von der Tastatur anbringen kann. Unten sind die Teile, die ich bisher zusammengestellt habe, und Erklärungen, warum ich das so entschieden habe.
0. Grundlegende Abstraktionen
Bezeichnungen für Scankodencodes. Damit muss unbedingt etwas geschehen, denn bloße Zahlen sind absolut nicht lesbar (das sage ich aus Erfahrung).
- Ich habe „ecodes.go“ aus „golang-evdev“ extrahiert (ich war zu faul, um die Primärquelle zu überprüfen, obwohl der Autor sie recht kultiviert angibt). Ich habe ein wenig (vorerst) die wirklich grässlichen Namen korrigiert. Zum Beispiel „LEFTBRACE“ → „L_BRACE“. ).
Zusätzlich habe ich das Konzept „Tasten mit Zustand“ eingeführt, da die verwendete reguläre Grammatik keine langen Passagen zulässt. - (Aber es ermöglicht Prüfungen mit minimalen Overheads. Wenn nur die „direkte“ Aufzeichnung verwendet wird.) (Зато позволяет проверять с минимальными накладками. Если использовать только «прямую» запись.)
- Ein integrierter „Deduplicator“ für Tastenanschläge wird implementiert. So wird der Zustand „Wiederholung“ = 2 aufgezeichnet. eins vorherigen
1. Template-Bereich
[Templates] # "@name@" zur Vereinfachung von Ausdrücken
# Wörter können aus diesen Zeichen bestehen (Regex)
"WORD" = "([0-9A-Z`;']|[LR]_BRACE|KOMMA|PUNKT|SCHRÄGSTRICH|KP[0-9])"
Woraus besteht ein Wort der menschlichen Sprache mit phonetischer Aufzeichnung (das sind Grafeme aka „Schriftzeichen“)? Какая-то ужасная «простыня». Поэтому сразу закладываю понятие «шаблона».
2. Was zu tun ist, wenn etwas gedrückt wird (ein neuer Scan-Code ist eingetroffen)
[ActionKeys]
# Taste sammeln und Test für die Befehlssequenz durchführen
# !!! Wiederholungscodes (code=2) müssen einmal pro Taste gesammelt werden!
Hinzufügen = ["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 fallen lassen, einschließlich dieser. Dies ist die Standardaktion.
Fallen lassen = ["ESC", "-", "TAB", "ENTER", "KPENTER", "LINEFEED..POWER"]
# Extraspeicher für diese Tasten, wenn eine im „down“-Zustand ist.
# Zustand wird über „OFF:“|„ON:“ Bedingungen in der Aktion überprüft.
# (Außerdem muss der Zustand dieser Tasten zwischen den Puffervorgängen 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.
# Z. B. 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 „nur für den Fall“ habe ich in den Code xswitcher zur Auffangung von „Überraschungen“ eingefügt).
Ich habe im Inneren das Ausfüllen des Arrays mit Links zu den Funktionen „was tun“ beschrieben. Auf Golang ist das (überraschend) sich als praktisch und offensichtlich herausgestellt.
- Ich plane, „Drop“ an dieser Stelle auf ein Minimum zu kürzen. Zugunsten einer flexibleren Verarbeitung (zeige ich unten).
3. Tabelle mit Fensterklassen
# 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. Einfacher ging es auf Anhieb nicht. Je nach aktuell aktivem Fenster können Optionen ausgewählt werden:
- Ihr eigenes Set an „Tastenkombinationen“ „Actions = …“. Wenn nicht / leer — nichts tun.
- Der Schalter „MouseClickDrops“ — was bei einem Mausklick zu tun ist. Da es am Einschaltpunkt xswitcher keine Details darüber gibt, „wohin geklickt wird“, setzen wir den Puffer standardmäßig zurück. Aber in Terminals kann man das so belassen. (in der Regel).
4. Eine (oder mehrere) Sequenzen von Tastendrücken 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 Hooks beginnen mit „Action.“. Da TOML v1.4, müssen Namen mit Punkten in Anführungszeichen stehen.
Für jeden sollte der entsprechende Abschnitt beschrieben werden mit demselben Namen..
Um den Leuten nicht mit "nackten" Regularien (aus Erfahrung, ihre schreiben-te kann einer von zehn Profis), füge ich sofort zusätzliche Syntax hinzu.
- "OFF:" (oder "ON:") vor regexp (regulärem Ausdruck) erfordert, dass die folgenden Tasten losgelassen (oder gedrückt) werden.
Dann werde ich einen "unfairen" regulären Ausdruck machen. Mit separater Überprüfung der Teile zwischen den Pipes "|". Zum Ziel, die Anzahl der Einträge wie "[LR]_SHIFT" zu reduzieren (wo es offensichtlich nicht nötig ist). - "SEQ:" Wenn die vorherige Bedingung erfüllt ist (oder fehlt), überprüfen wir als nächstes den "gewöhnlichen" regulären Ausdruck. Für Details verweise ich sofort auf die Bibliothek "regexp". Weil ich selbst bisher nicht die Mühe gemacht habe, herauszufinden, wie kompatibel sie mit meinen Lieblings-pcre ("perl compatible") sind.
- Der Ausdruck wird in der Form "TASTE_1: CODE1, TASTE_2: CODE2" usw., in der Reihenfolge des Eintreffens der Scancode.
- Die Überprüfung wird immer am Ende der Sequenz "festgehalten", deshalb muss "$" am Ende nicht hinzugefügt werden.
- Alle Überprüfungen in einer Zeile werden nacheinander ausgeführt. und werden durch "I" zusammengeführt. Da der Wert jedoch als Array beschrieben ist, kann man nach dem Komma eine alternative Überprüfung angeben. Falls das aus irgendeinem Grund notwendig ist.
- Bedeutung "SeqLength = 8" beschränkt die Größe des Puffers, auf den sich alle Überprüfungen beziehen. Da ich im Leben (bis jetzt) noch nie auf unendliche Ressourcen gestoßen bin.
5. Zuweisung von 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 Hauptsächliche hier ist — "Action = [Array]". Ähnlich wie im vorherigen Abschnitt gibt es eine begrenzte Anzahl integrierter Aktionen. Und eine unbegrenzte Möglichkeit der Integration. (man schreibt "Action.XXX" und tut nicht faul, eine weitere Sektion dafür zu beschreiben).
Darunter fällt auch, dass das Umschreiben eines Wortes in der korrigierten Eingabesprache in zwei Teile unterteilt wird: "Ändere das Layout wie dort angegeben" und "Umschreiben" ("RetypeWord").
Die restlichen Parameter werden in einem "Wörterbuch" aufgezeichnet ("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 Bündel beschrieben werden (Sektionen). Oder man kann sie aufteilen. Wie ich oben gezeigt habe.
Ich lege sofort die Aktion "Exec" fest — ein externes Skript ausführen. Mit der Option, ihm den aufgezeichneten Puffer in stdin zu übergeben.
- «Warten = 1» — auf den Abschluss des gestarteten Prozesses warten.
- Vielleicht möchte man „nebenbei“ zusätzliche Informationen wie den Namen der Fensterklasse, aus der abgefangen wurde, bereitstellen.
„Möchten Sie Ihren eigenen Handler anschließen? Hier lang.“
Uff (ausgeatmet). Ich glaube, ich habe nichts vergessen.
Oops! Ja, ich habe nichts vergessen…
Aber wo ist die Startkonfiguration? Im Hard-Code? Etwa so:
[ScanDevices]
# Muss beim Start vorhanden sein. Selbstwiederherstellung, falls es jünger als 30 Sekunden ist
Test = "/dev/input/event0"
Respawn = 30
# Suchmaske
Suche = "/dev/input/event*"
# In meinen ThinkPads gibt es solche Pseudo-Tastaturen mit Unmengen von unnötigen Ereignissen
Umgehen = "(?i)Video|Camera" # "(?i)" unterscheidet sich offensichtlich von "klassischen" PCREs.
Wo habe ich vergessen/mich geirrt (ohne das geht es nicht), ich hoffe sehr, dass die aufmerksamen Leser nicht faul sind, und das hinweisen.
Viel Erfolg!
Quelle: habr.com
