The layout corrector 'xswitcher' for linux: step two

Since previous post (the switcher at the 'proof of concept' stage) received quite a bit of constructive feedback (which is nice), I continued to spend my free time developing the project. Now I want to take a bit of yours... The second step will be somewhat unusual: proposing/discussing the configuration design.

The layout corrector 'xswitcher' for linux: step two

It seems that normal programmers find setting up all these knobs dreadfully boring.

To be more specific, here is an example of what I am dealing with.
Overall, well-conceived (and nicely implemented) Apache Kafka & ZooKeeper.
-- Configuration? But that's boring! Slap-dash XML (because 'out of the box').
-- Oh, but you also want ACLs? But that's so tedious! Slap-dash... Something like that.

But in my work, it's exactly the opposite. Correctly (unfortunately, it's almost never perfect the first time) the constructed model allows for easy and effortless further development (well, almost) of the schema.

I recently came across an article on Habr about the challenging work of data scientists...
It turns out that this aspect fully manifests in their field. And in my experience, as they say, it's a 'light version'. Multi-volume models, seasoned programmers with OOP skills, etc. — all this will come later, if it takes off. But the constructor needs to start somewhere right here and now.

Getting to the point. As a syntactic basis, I chose TOML from this gentleman.

Because it (TOML) is user-editable on one side, but on the other side, it translates 1:1 into any of the more common syntaxes: XML, JSON, YAML.
Moreover, the implementation I used from 'github.com/BurntSushi/toml', while not the most fashionable (still syntax 1.4), is syntactically compatible with the same ('embedded') JSON.

That is, if desired, one can simply say 'go away with your TOML, I want XXX' and 'patch' the code with just one line.

Thus, if one wants to write windows for configuring the switcher (definitely not me) problems 'with this damn config' are not anticipated.

For all others, the syntax based on 'key = value' (and literally a couple of options more complex, like = [some, array]) I consider
intuitively convenient.
Interestingly, I also had my 'fire sparked' around the same time (around 2013). Only, unlike me, the author of TOML approached it with the right scale.

So now it's easier for me to adjust its implementation to my needs rather than the other way around.

In general, we take TOML (which is very similar to the old Windows INI). And we stand the configuration in which we describe how to attach a series of hooks depending on the set of recent scan codes from the keyboard. Below in pieces — what has been achieved so far. And explanations about why I decided this way.

0. Basic Abstractions

  • Denotations of scan codes. Something needs to be done about this, as just numerical codes are completely not human-readable (that's what I mean) loloswitcher).
    I extracted "ecodes.go" from "golang-evdev" (I was too lazy to dig into the original source, although it is quite culturally indicated by the author). I also made some minor (for now) fixes to the really ugly parts. Like "LEFTBRACE" → "L_BRACE".
  • Additionally, I introduced the concept of "stateful keys". Since the used regular grammar does not accommodate long passages. (At least it allows checking with minimal overhead. If only "direct" recording is used.)
  • There will be a built-in "deduplicator" for the pressed keys. Thus, the state "repeat"=2 will be recorded one time.

1. Templates 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 a word of human language consists of with phonetic transcription (unlike graphemes aka "characters")? Какая-то ужасная «простыня». Поэтому сразу закладываю понятие «шаблона».

2. What to do when something is pressed (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 with 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 included a catch for "surprises" in the code for hswitcher).
Inside, I detailed the filling of the array with links to functions "what to do". In golang, this (unexpectedly) turned out to be convenient and obvious.

  • I plan to minimize «Drop» in this place. 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"

Table rows are in double square brackets with its name. It didn't work out simply right away. Depending on the current active window, options can be selected:

  • Your own set of «hotkeys» «Actions = …». If not/empty — do nothing.
  • The switch «MouseClickDrops» — what to do when a mouse click is detected. Since there are no details in the xswitcher activation point about 'where they click', we reset the buffer by default. However, in terminals (for example), this may not be necessary. (usually).

4. One (or several) sequences of key presses trigger a specific 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)" ]

I divided hooks into two types. Built-in, with 'meaningful' names (NewWord, NewSentence, Compose) and programmable.

Programmable names start with 'Action.'. Since TOML v1.4, names with periods must be in quotes.

Below, each must have a section described with the same name.

To avoid blowing people's minds with 'naked' regular expressions (from experience, they to write-can be done by one in ten professionals), I immediately introduce additional syntax.

  • «OFF:» (or «ON:») before the regexp (regular expression) requires that the buttons specified afterwards be released (or pressed).
    Next, I plan to create a 'dishonest' regular expression. With separate checks for pieces between pipes "|". Aimed at reducing the number of records like "[LR]_SHIFT" (where it's clearly unnecessary).
  • «SEQ:» If the previous condition is met (or absent), check relative to the 'ordinary' regular expression. For details, I will send you to the library "regexp". Because I still haven't bothered to figure out the degree of compatibility with my favorite pcre ('perl compatible').
  • The expression is written in the form «BUTTON_1: CODE1, BUTTON_2: CODE2» and so on, in the order of arrival of scan codes.
  • The check always 'presses' against the end of the sequence, so you don't need to add "$" at the end.
  • All checks in one line are executed one after another and are combined by 'AND'. But since the value is described as an array, you can write an alternative check after the comma. If this is needed for some reason.
  • Value «SeqLength = 8» limits the buffer size for which all checks are performed. Since I have never encountered infinite resources in my life (until now).

5. Hook assignments detailed 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 point here is— “Action = [Array]”. Similar to the previous section, there is a limited set of built-in actions. However, there is no fundamental limit on the possibility of integration. (write “Action.XXX” and take the time to describe another section for it).
This includes the re-typing of a word in a corrected layout, which is divided into two parts: “change the layout as specified over there” and “retype” (“RetypeWord”).

Other parameters are recorded in 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 pile (sections). Or they can be separated out. As I showed above.

I immediately lay out the action “Exec” — to execute an external script, with the option to push the recorded buffer into stdin.

  • “Wait = 1” — wait for the launched process to finish.
  • Probably, “on top of that” you will want to set additional information in the environment, such as the name of the class window from which it was intercepted.
    “Want to connect your handler? Here you go.”

Phew (exhaled). It seems I didn't forget anything.

Oops! Aha, I didn't forget...
But where is the launch configuration? In hard code? Something like this:

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

And where did I forget/make a mistake (without this — it won't work), I really hope that attentive readers won't hesitate to point it out.

Good luck!

Source: habr.com

Buy reliable website hosting with DDoS protection, VPS VDS servers 🔥 Buy reliable website hosting with DDoS protection, VPS VDS servers | ProHoster