Xswitcher layout corrector for linux: step two

As previous publication (xswitcher at the proof of concept stage) received quite a lot of constructive feedback (which is nice), I continued to spend my free time developing the project. Now I want to spend some of your... The second step will not be quite familiar: proposal / discussion of the design of the configuration.

Xswitcher layout corrector for linux: step two

Somehow it turns out that it is wildly boring for normal programmers to set up all these twists.

In order not to be unfounded, inside is an example of what I'm dealing with.
Great overall conceived (and well implemented) Apache Kafka & ZooKeeper.
— Configuration? But it's boring! Tyap-bloop xml (because "out of the box").
- Oh, do you also want ACL? But it's so annoying! Tap-blunder ... Something like that.

In my work, it's exactly the opposite. Right (alas, the first time almost never) the built model allows further easily and naturally (Almost) assemble the diagram.

Recently I came across an article on Habré about the hard work of data-scientists ...
It turns out that this moment they are fully realized. And in my practice, as they say, "light version". Multi-volume models, seasoned programmers with OOP at the ready, etc. - all this will appear later when / if it takes off. And the designer needs to start here and now with something.

Get to the point. As a syntactic basis, I took TOML from this citizen.

Because he (TOML) on the one hand human-editable. On the other hand, it is translated 1:1 into any of the more common syntaxes: XML, JSON, YAML.
Moreover, the implementation I used from “github.com/BurntSushi/toml”, although not the most fashionable (still syntax 1.4), is syntactically compatible with the same (“embedded”) JSON.

That is, if you wish, you can simply say “go to the forest with this TOML of yours, I want XXX” and “patch” the code with just one line.

Thus, if you wish, write some windows to configure xswitcher (I'm not sure) problems “with your fucking config” are not expected.

For all others, the syntax is based on "key = value" (and literally a couple of more complicated options, like = [some kind of array]) I suppose
intuitively convenient.
Curiously, at the very "burnt" around the same time (around 2013). Only, unlike me, the author of TOML went on a grand scale.

Therefore, now it is easier for me to adjust its implementation for myself, and not vice versa.

In general, we take TOML (very similar to the old Windows INI). And we have a configuration in which we describe how to attach a series of hooks depending on the set of the latest skin codes from the keyboard. Below in pieces - what happened at the moment. And an explanation of why I decided so.

0. Basic abstractions

  • Scan code designations. Something must be done with this, since just digital codes are absolutely not human-readable (this is me in the garden loloswitcher).
    I shook out “ecodes.go” from “golang-evdev” (I was too lazy to go to the source, although the author has it quite culturally indicated). A little bit (so far) corrected a very fearful one. Type "LEFTBRACE" → "L_BRACE".
  • In addition, he introduced the concept of "keys with a state". Since the regular grammar used is not conducive to long passages. (But it allows you to check with minimal overhead. If you use only "direct" recording.)
  • There will be a built-in "deduplicator" of the clicked one. So the state "repeat"=2 would be written one time.

1. Template section

[Templates] # "@name@" to simplify expressions
 # Words can consist of these chars (regex)
 "WORD" = "([0-9A-Z`;']|[LR]_BRACE|COMMA|DOT|SLASH|KP[0-9])"

What does a human language word with phonetic notation consist of? (Is it a matter of graphemes aka "hieroglyphs")? Some kind of terrible "sheet". Therefore, I immediately lay the concept of "template".

2. What to do when something is clicked (another scan code has arrived)

[ActionKeys]
 # Collect key and do the test for command sequence
 # !!! Repeat codes (code=2) must be collected once per key!
 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"]

 # Drop all collected keys, including this.  This is default action.
 Drop = ["ESC", "-", "TAB", "ENTER", "KPENTER", "LINEFEED..POWER"]
 # Store extra map for these keys, when any is in "down" state.
 # State is checked via "OFF:"|"ON:" conditions in action.
 # (Also, state of these keys must persist between buffer drops.)
 # ??? How to deal with CAPS and "LOCK"-keys ???
 StateKeys = ["L_CTRL", "L_SHIFT", "L_ALT", "L_META", "CAPS", "N_LOCK", "S_LOCK",
              "R_CTRL", "R_SHIFT", "R_ALT", "R_META"]

 # Test only, but don't collect.
 # E.g., I use F12 instead of BREAK on dumb laptops whith shitty keyboards (new ThinkPads)
 Test = ["F1..F10", "ZENKAKUHANKAKU", "102ND", "F11", "F12",
          "RO..KPJPCOMMA", "SYSRQ", "SCALE", "HANGEUL..YEN",
          "STOP..SCROLLDOWN", "NEW..MAX"]

There are 768 codes in total. (But "just in case" I inserted catching "surprises" into the xswitcher code).
Inside, I painted filling the array with links to the “what to do” functions. In golang this is (suddenly) turned out to be convenient and obvious.

  • "Drop" in this place I plan to reduce to a minimum. In favor of more flexible processing (I will show below).

3. Table with window classes

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

The rows of the table are in double square brackets with its name. It didn't work out any easier. Depending on the currently active window, you can select options:

  • Your own set of "hot keys" "Actions = ...". If not/empty, do nothing.
  • MouseClickDrops toggle - what to do when a mouse click is detected. Since the xswitcher switch-on point does not contain any details of “where it is clicked”, by default we reset the buffer. But in terminals (for example) you can not do this (usually).

4. One (or more) sequences of clicks trigger one or another 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)" ]

Hooks are divided into two types. Built-in, with "talking" names (NewWord, NewSentence, Compose) and programmable.

Programmable names begin with "Action.". Because TOML v1.4, names with dots must be in quotes.

Each section should be described below. with the same name.

In order not to blow people's brains with “naked” regular seasons (according to experience, their writemaybe one in ten professional), immediately implement additional syntax.

  • "OFF:" (or "ON:") before regexp (regular expression) require that the following buttons be released (or pressed).
    Next I'm going to make a "dishonest" regular expression. With separate checking of pieces between "|" pipes. In order to reduce the number of records like "[LR]_SHIFT" (where it is clearly not necessary).
  • SEQ: If the previous condition is met (or absent), then we check against the "ordinary" regular expression. For details, I immediately send to ^W to the "regexp" library. Because he himself has not yet bothered to find out the degree of compatibility with my favorite pcre ("perl compatible").
  • The expression is written as "BUTTON_1: CODE1, BUTTON_2: CODE2" etc., in the order of receipt of the scan codes.
  • Validation is always "pressed" to the end of the sequence, so "$" does not need to be added to the tail.
  • All checks in one line are executed one after the other and are combined by "I". But since the value is described as an array, you can write an alternative check after the comma. If it's necessary for some reason.
  • Value "SeqLength=8" limits the size of the buffer against which all checks are performed. Because I have not (so far) seen infinite resources in my life.

5. Setting the hooks described in the previous section

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

The main thing here is "Action = [Array]". Similar to the previous section, there is a limited set of built-in actions. And the possibility of docking, which is not limited in principle (write "Action.XXX" and don't be too lazy to paint another section for it).
In particular, word typing in the corrected layout is divided into two parts: "Change the layout as given over there" и "retype" ("RetypeWord").

The remaining parameters are written to the "dictionary" ("map" in golang) for a given action, their list depends on what is written in "Action".

Several different actions can be described in one heap (sections). And you can take it apart. As I showed above.

Immediately lay the action "Exec" - execute an external script. With the option to push the written buffer into stdin.

  • "Wait = 1" - wait for the running process to finish.
  • Probably, “to the heap” you will want to put additional in the environment. information such as the class name of the window from which it was intercepted.
    “Do you want to connect your handler? Here you are."

Phew (exhaled). Looks like I didn't forget anything.

Op! Yep, didn't forget...
Where is the launch configuration? In hardcode? Like that:

[ScanDevices]
 # Must exist on start. Self-respawn in case it is younger then 30s
 Test = "/dev/input/event0"
 Respawn = 30
 # Search mask
 Search = "/dev/input/event*"
 # In my thinkPads there are such a pseudo-keyboards whith tons of unnecessary events
 Bypass = "(?i)Video|Camera" # "(?i)" obviously differs from "classic" pcre's.

Where did I forget/mistake? (no way without it), I really hope that attentive readers will not be too lazy to poke their noses.

Good luck!

Source: habr.com

Add a comment