Xswitcher layout corrector for linux: step two

Since the previous publication (xswitcher at the “proof of concept” stage) received a lot of constructive reviews (which is nice) , I continued to spend my free time on the development of the project. Now I want to spend a little of yours ... The second step will not be quite familiar: suggestion / discussion of configuration design.



Somehow it turns out that normal programmers are wildly bored to tune all these twists.

In order not to be unfounded, inside is an example of what I'm dealing with.
( ) Apache Kafka & ZooKeeper.
— ? ! - xml ( « »).
— , ACL ? ! -… - .

And in my work - exactly the opposite. Correctly (alas, from the first time almost never) the constructed model allows you to easily and easily (well, almost) assemble the circuit.

Recently article on Habré came across an article about hard work of data-scientists ...
, . , , « ». , .. — , / . - .

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

Because it (TOML) is on one hand human-editable. And on the other - it translates 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 ("built-in") JSON.

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

Thus, if you want to write some windows (certainly not me) to configure xswitcher, there are no problems "with this your fucking config".

For all the others, the syntax based on "key = value" (and literally a couple of options is more complicated, type = [some, that, array]) I suppose
intuitively convenient.
, «» ( 2013 ). , , TOML .

, .

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 scan codes from the keyboard. Below in pieces - what happened at the moment. And the explanation of what I decided so.

0. Basic abstractions


  • -. - , - ( loloswitcher).
    «ecodes.go» «golang-evdev» ( , ). () . «LEFTBRACE» → «L_BRACE».
  • « ». . ( . «» .)
  • «» . , «»=2 .

1.


[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 (or is it a grapheme aka “hieroglyphs” case) ? Some kind of terrible "sheet." Therefore, I immediately lay the concept of “template”.

2. What to do when something is pressed (the next 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"]

A total of 768 codes are provided. (But, just in case, I inserted a catch of "surprises" in the xswitcher code).
Inside I painted filling the array with links to the "what to do" functions. On golang, it (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. Window class label


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

Table rows are in double square brackets with its name. Easier to go did not work. Depending on the current active window, you can choose the options:

  • « » «Actions = …». / — .
  • «MouseClickDrops» — . xswitcher « », - . () ( ).

4. ( )


# 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, dotted names must be in quotation marks.

A section with the same name should be described below for each .

In order not to blow up people's brains with “bare” regulars (from experience, one out of ten professionals can write them ), I immediately introduce an additional syntax.

  • «OFF:» ( «ON:») regexp ( ) ( ).
    «» . "|". "[LR]_SHIFT" ( ).
  • «SEQ:» ( ), «» . ^W «regexp». pcre («perl compatible»).
  • «_1: 1, _2: 2» .., -.
  • «» , "$" .
  • «». , . - .
  • «SeqLength = 8» , . .. ( ) .

5. ,


# 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 unlimited in principle the possibility of docking (write "Action.XXX" and not be too lazy to paint another section for it) .
In particular, the word re-typing in the corrected layout is divided into two parts: “change the layout as it is set there” and “re-type” (“RetypeWord”) .

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

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

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

  • “Wait = 1” - wait for the completion of the running process.
  • Probably, "to the heap" will want to put in the environment extra. information such as the name of the window class from which it is intercepted.
    “Want to hook up your handler?” To you here. ”

Phew (exhaled). It seems to have forgotten nothing.

Oops! Yeah, I didn’t forget ...
? -́? :

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


And where I forgot / made a mistake (without this - no way) , I really hope that attentive readers are not too lazy to poke their nose.

Good luck

All Articles