Code Monkey home page Code Monkey logo

keyszer's Introduction

keyszer - a smart key remapper for Linux/X11

latest version python 3.10 license code quality discord

open issues help welcome issues good first issue build and CI status

Keyszer is a smart key remapper for Linux (and X11) written in Python. It's similar to xmodmap but allows far more flexible remappings. Keyszer was forked from xkeysnail which no longer seems actively maintained.

How does it work?

Keyszer works at quite a low-level. It grabs input directly from the kernel's evdev input devices ( /dev/input/event*) and then creates an emulated uinput device to inject those inputs back into the kernel. During this process the input stream is transformed on the fly as necessary to remap keys.

Upgrading from xkeysnail

Key Highlights

  • Low-level library usage (evdev and uinput) allows remapping to work from the console all the way into X11.
  • High-level and incredibly flexible remapping mechanisms:
    • per-application keybindings - bindings that change depending on the active X11 application or window
    • multiple stroke keybindings - Ctrl+x Ctrl+c could map to Ctrl+q
    • very flexible output - Ctrl-s could type out :save, and then hit enter
    • stateful key combos - build Emacs style combos with shift/mark
    • multipurpose bindings - a regular key can become a modifier when held
    • arbitrary functions - a key combo can run custom Python function

New Features (since xkeysnail 0.4.0)

  • simpler and more flexible configuration scripting APIs
  • better debugging tools
    • configurable EMERGENCY EJECT hotkey
    • configurable DIAGNOSTIC hotkey
  • fully supports running as semi-privileged user (using root is now deprecated)
  • adds include to allow config to pull in other Python files
  • adds throttle_delays to allow control of output speed of macros/combos
  • adds immediately to nested keymaps
  • adds Meta, Command and Cmd aliases for Super/Meta modifier
  • add C combo helper (eventually to replace K)
  • supports custom modifiers via add_modifier (such as Hyper)
  • supports Fn as a potential modifier (on hardware where it works)
  • adds bind helper to support persistent holds across multiple combos
    • most frequently used for persistent Mac OS style Cmd-tab app switcher panels
  • adds --check for checking the config file for issues
  • adds wm_name context for all conditionals (PR #40)
  • adds device_name context for all conditionals (including keymaps)
  • (fix) xmodmap cannot be used until some keys are first pressed on the emulated output
  • (fix) ability to avoid unintentional Alt/Super false keypresses in many setups
  • (fix) fixes multi-combo nested keymaps (vs Kinto's xkeysnail)
  • (fix) properly cleans up pressed keys before termination
  • individually configurable timeouts (multipurpose and suspend)
  • (fix) removed problematic launch macro
  • (fix) suspend extra keys during sequential sequences to create less key "noise"
  • (fix) handle X Display errors without crashing or bugging out

Installation

Requires Python 3.

Over time we should add individual instructions for various distros here.

From source

Just download the source and install.

git clone https://github.com/joshgoebel/keyszer.git
cd keyszer
pip3 install --user --upgrade .

For testing/hacking/contributing

Using a Python venv might be the simplest way to get started:

git clone https://github.com/joshgoebel/keyszer.git
cd keyszer
python -m venv .venv
source .venv/bin/activate
pip3 install -e .
./bin/keyszer -c config_file

System Requirements

Keyszer requires read/write access to:

  • /dev/input/event* - to grab input from your evdev input devices
  • /dev/uinput - to provide an emulated keyboard to the kernel

Running as a semi-privileged user

It's best to create an entirely isolated user to run the keymapper. Group or ACL based permissions can be used to provide this user access to the necessary devices. You'll need only a few udev rules to ensure that the input devices are all given correct permissions.

ACL based permissions (narrow, more secure)

First, lets make a new user:

sudo useradd keymapper

...then use udev and ACL to grant our new user access:

Manually edit /etc/udev/rules.d/90-keymapper-acl.rules to include the following:

KERNEL=="event*", SUBSYSTEM=="input", RUN+="/usr/bin/setfacl -m user:keymapper:rw /dev/input/%k"
KERNEL=="uinput", SUBSYSTEM=="misc", RUN+="/usr/bin/setfacl -m user:keymapper:rw /dev/uinput"

...or do it by copypasting these lines into a shell:

cat <<EOF | sudo tee /etc/udev/rules.d/90-keymapper-acl.rules
KERNEL=="event*", SUBSYSTEM=="input", RUN+="/usr/bin/setfacl -m user:keymapper:rw /dev/input/%k"
KERNEL=="uinput", SUBSYSTEM=="misc", RUN+="/usr/bin/setfacl -m user:keymapper:rw /dev/uinput"
EOF

Group based permissions (slightly wider, less secure)

Many distros already have an input group; if not, you can create one. Next, add a new user that's a member of that group:

sudo useradd keymapper -G input

...then use udev to grant our new user access (via the input group):

Manually edit /etc/udev/rules.d/90-keymapper-input.rules to include the following:

SUBSYSTEM=="input", GROUP="input"
KERNEL=="uinput", SUBSYSTEM=="misc", GROUP="input"

...or do it by copypasting these lines into a shell:

cat <<EOF | sudo tee /etc/udev/rules.d/90-keymapper-input.rules
SUBSYSTEM=="input", GROUP="input"
KERNEL=="uinput", SUBSYSTEM=="misc", GROUP="input"
EOF

systemd

For a sample systemd service file for running Keyszer as a service please see keyszer.service.

Running as the Active Logged in User

This may be appropriate in some limited development scenarios, but is not recommended. Giving the active, logged in user access to evdev and uinput potentially allows all keystrokes to be logged and could allow a malicious program to take over (or destroy) your machine by injecting input into a Terminal session or other application.

It would be better to open a terminal, su to a dedicated keymapper user and then run Keyszer inside that context, as shown earlier.

Running as root

Don't do this, it's dangerous, and unnecessary. A semi-privileged user with access to only the necessary input devices is a far better choice.

Usage

keyszer

A successful startup should resemble:

keyszer v0.5.0
(--) CONFIG: /home/jgoebel/.config/keyszer/config.py
(+K) Grabbing Apple, Inc Apple Keyboard (/dev/input/event3)
(--) Ready to process input.

Limiting Devices

Limit remapping to specific devices with --devices:

keyszer --devices /dev/input/event3 'Topre Corporation HHKB Professional'

The full path or complete device name may be used. Device name is usually better to avoid USB device numbering jumping around after a reboot, etc...

Other Options:

  • -c, --config - location of the configuration file
  • -w, --watch - watch for new keyboard devices to hot-plug
  • -v - increase verbosity greatly (to help with debugging)
  • --list-devices - list out all available input devices

Configuration

By default we look for the configuration in ~/.config/keyszer/config.py. You can override this location using the -c/--config switch. The configuration file is written in Python. For an example configuration please see example/config.py.

The configuration API:

  • timeouts(multipurpose, suspend)
  • throttle_delays(key_pre_delay_ms, key_post_delay_ms)
  • wm_class_match(re_str)
  • not_wm_class_match(re_str)
  • add_modifier(name, aliases, key/keys)
  • modmap(name, map, when_conditional)
  • multipurpose_modmap(name, map, when_conditional)
  • keymap(name, map, when_conditional)
  • conditional(condition_fn, map) - used to wrap maps, applying them conditionally
  • dump_diagnostics_key(key)
  • emergency_eject_key(key)
  • include(relative_filename)

include(relative_filename)

Include a sub-configuration file into the existing config. This file is loaded and executed at the point of inclusion and shares the same global scope as the existing config. These files should be present in the same directory as your main configuration.

include("os.py")
include("apps.py")
include("deadkeys.py")

timeouts(...)

Configures the timing behavior of various aspects of the keymapper.

  • multipurpose - The number of seconds before a held multi-purpose key is assumed to be a modifier (even in the absence of other keys).
  • suspend - The number of seconds modifiers are "suspended" and withheld from the output waiting to see whether if they are part of a combo or if they may be the actual intended output.

Defaults:

timeouts(
    multipurpose = 1,
    suspend = 1,
)

throttle_delays(...)

Configures the speed of virtual keyboard keystroke output to deal with issues that occur in various situations with the timing of modifier key presses and releases being misinterpreted.

  • key_pre_delay_ms - The number of milliseconds to delay the press-release keystroke of the "normal" key after pressing modifier keys.
  • key_post_delay_ms - The number of milliseconds to delay the next key event (modifier release) after the "normal" key press-release event.

Defaults:

throttle_delays(
    key_pre_delay_ms    = 0,    # default: 0 ms, range: 0 to 150 ms, suggested: 1-50 ms
    key_post_delay_ms   = 0,    # default: 0 ms, range: 0 to 150 ms, suggested: 1-100 ms
)

Use the throttle delays if you are having the following kinds of problems:

  • Shortcut combos seeming to behave unreliably, sometimes as if the unmapped shortcut (or part of the unmapped shortcut) is being pressed at the same time.
  • Macros of sets of keystrokes, or strings or Unicode sequences processed by the keymapper into keystrokes, having various kinds of failures, such as:
    • Missing characters
    • Premature termination of macro
    • Shifted or uppercase characters coming out as unshifted/lowercase
    • Unshifted or lowercase characters coming out as shifted/uppercase
    • Unicode sequences failing to complete and create the desired Unicode character

Suggested values to try if you are in a virtual machine and having major problems with even common shortcut combos:

  • key_pre_delay_ms: 40
  • key_post_delay_ms: 70

The post delay seems a little more effective in testing, but your situation may be different. For a bare-metal install where you are just having a few glitches in macro output, try much smaller delays:

  • key_pre_delay_ms: 0.1
  • key_post_delay_ms: 0.5

These are just examples that have worked fairly well in current testing on machines that have had these issues.

dump_diagnostics_key(key)

Configures a key that when hit will dump additional diagnostic information to STDOUT.

dump_diagnostics_key(Key.F15)  # default

emergency_eject_key(key)

Configures a key that when hit will immediately terminate keyszer; useful for development, recovering from bugs, or badly broken configurations.

emergency_eject_key(Key.F16)  # default

add_modifier(name, aliases, key/keys)

Allows you to add custom modifiers and then map them to actual keys.

add_modifier("HYPER", aliases = ["Hyper"], key = Key.F24)

Note: Just adding HYPER doesn't necessarily make it work with your software, you may still need to configure X11 setup to accept the key you choose as the "Hyper" key.

wm_class_match(re_str)

Helper to make matching conditionals (and caching the compiled regex) just a tiny bit simpler.

keymap("Firefox",{
    # ... keymap here
}, when = wm_class_match("^Firefox$"))

not_wm_class_match(re_str)

The negation of wm_class_match, matches only when the regex does NOT match.

modmap(name, mappings, when_conditional = None)

Maps a single physical key to a different key. A default modmap will always be overruled by any conditional modmaps that apply. when_conditional can be passed to make the modmap conditional. The first modmap found that includes the pressed key and matches the when_conditional will be used to remap the key.

modmap("default", {
    # mapping caps lock to left control
    Key.CAPSLOCK: Key.LEFT_CTRL
})

If you don't create a default (non-conditional) modmap a blank one is created for you. For modmap both sides of the pairing will be Key literals (not combos).

multipurpose_modmap(name, mappings)

Used to bestow a key with multiple-purposes, both for regular use and for use as a modifier.

multipurpose_modmap("default",
    # Enter is enter if pressed and immediately released...
    # ...but Right Control if held down and paired with other keys.
    {Key.ENTER: [Key.ENTER, Key.RIGHT_CTRL]}
)

keymap(name, mappings)

Defines a keymap of input combos mapped to output equivalents.

keymap("Firefox", {
    # when Cmd-S is input instead send Ctrl-S to the output
    C("Cmd-s"): C("Ctrl-s"),
}, when = lambda ctx: ctx.wm_class == "Firefox")

Because of the when conditional this keymap will only apply for Firefox.

The argument mappings is a dictionary in the form of { combo: command, ...} where combo and command take following forms:

  • combo: Combo to map, specified by K(combo_str)
  • command: one of the following
    • K(combo_str): Type a specific key combo to the output.
    • [command1, command2, ...]: Execute multiple commands sequentially.
    • { ... }: Sub-keymap. Used to define Multiple Stroke Keys.
    • escape_next_key: Escape the next key pressed.
    • ignore_key: Ignore the key that is pressed next. (often used to disable native combos)
    • bind: Bind an input and output modifier together such that the output is not lifted until the input is.
    • arbitrary function: The function is executed and the returned value (if any) is used as a command.

The argument name specifies the keymap name. Every keymap has a name - using default is suggested for a non-conditional keymap.

conditional(fn, map)

Applies a map conditionally, only when the fn function evaluates True. The below example is a modmap that is only active when the current WM_CLASS is Terminal.

conditional(
    lambda ctx: ctx.wm_class == "Terminal",
    modmap({
        # ...
    })
)

The context object passed to the fn function has several attributes:

  • wm_class - the WM_CLASS of the [input] focused X11 window
  • wm_name - the WM_NAME of the [input] focused X11 window
  • device_name - name of the device where an input originated
  • capslock_on - state of CapsLock (boolean)
  • numlock_on - state of NumLock (boolean)

Note: The same conditional fn can always be passed directly to modmap using the when argument.


Marks

TODO: need docs (See issue #8)

Combo Specifications

The Combo specification in a keymap is written in the form of C("(<Modifier>-)*<Key>").

<Modifier> is one of the following:

  • C or Ctrl -> Control key
  • Alt -> Alt key
  • Shift -> Shift key
  • Super, Win, Command, Cmd, Meta -> Super/Windows/Command key
  • Fn -> Function key (on supported keyboards)

You can specify left/right modifiers by adding the prefixes L or R.

<Key> is any key whose name is defined in key.py.

Some combo examples:

  • C("LC-Alt-j"): left Control, Alt, j
  • C("Ctrl-m"): Left or Right Control, m
  • C("Win-o"): Cmd/Windows, o
  • C("Alt-Shift-comma"): Alt, Left or Right Shift, comma

Multiple Stroke Keys

To use multiple stroke keys, simply define a nested keymap. For example, the following example remaps C-x C-c to C-q.

keymap("multi stroke", {
    C("C-x"): {
      C("C-c"): C("C-q"),
    }
})

If you'd like the first keystroke to also produce it's own output, immediately can be used:

keymap("multi stroke", {
  C("C-x"): {
    # immediately output "x" when Ctrl-X is pressed
    immediately: C("x"),
    C("C-c"): C("C-q"),
  }
})

Finding out the proper Key.NAME literal for a key on your keyboard

From a terminal session run evtest and select your keyboard's input device. Now hit the key in question.

Event: time 1655723568.594844, type 1 (EV_KEY), code 69 (KEY_NUMLOCK), value 1
Event: time 1655723568.594844, -------------- SYN_REPORT ------------

Above I've just pressed "clear" on my numpad and see code 69 (KEY_NUMLOCK) in the output. For Keyszer this would translate to Key.NUMLOCK. You can also browse the full list of key names in the source.

Finding an Application's WM_CLASS and WM_NAME using xprop

Use the xprop command from a terminal:

xprop WM_CLASS WM_NAME

...then click an application window. Let's try it with Google Chrome:

WM_CLASS(STRING) = "google-chrome", "Google-chrome"
WM_NAME(UTF8_STRING) = "README - Google Chrome"

Use the second WM_CLASS value (in this case Google-chrome) when matching context.wm_class.

Example of Case Insensitive Matching

terminals = ["gnome-terminal","konsole","io.elementary.terminal","sakura"]
terminals = [term.casefold() for term in terminals]
USING_TERMINAL_RE = re.compile("|".join(terminals), re.IGNORECASE)

modmap("not in terminal", {
    Key.LEFT_ALT: Key.RIGHT_CTRL,
    # ...
    }, when = lambda ctx: ctx.wm_class.casefold() not in terminals
)

modmap("terminals", {
    Key.RIGHT_ALT: Key.RIGHT_CTRL,
    # ...
    }, when = lambda ctx: USING_TERMINAL_RE.search(ctx.wm_class)
)

FAQ

Can I remap the keyboard's Fn key?

It depends. Most laptops do not allow this as the Fn keypress events are not directly exposed to the operating system. On some keyboards, it's just another key. To find out you can run evtest. Point it to your keyboard device and then hit a few keys; then try Fn. If you get output, then you can map Fn. If not, you can't.

Here is an example from a full size Apple keyboard I have:

Event: time 1654948033.572989, type 1 (EV_KEY), code 464 (KEY_FN), value 1
Event: time 1654948033.572989, -------------- SYN_REPORT ------------
Event: time 1654948033.636611, type 1 (EV_KEY), code 464 (KEY_FN), value 0
Event: time 1654948033.636611, -------------- SYN_REPORT ------------

What if my keyboard seems laggy or is not repeating keys fast enough?

You likely need to set the [virtual] keyboards repeat rate to match your actual keyboard.

Here is the command I use:

xset r rate 200 20

For best results your real keyboard and Keyszer [virtual] keyboard should have matching repeat rates. That seems to work best for me. Anytime you restart keyszer you'll need to reconfigure the repeat rate because each time a new virtual keyboard device is created... or maybe it's that there is only a single repeat rate and every time you "plug in" a new keyboard it changes?

If you could shed some light on this, please get in touch.

Does Keyszer support FreeBSD/NetBSD or other BSDs?

Not at the moment, perhaps never. If you're an expert on the BSD kernel's input layers please join the discussion. I'm at the very least open to the discussion to find out if this is possible, a good idea, etc...

Does this work with Wayland?

Not yet. This is desires but seems impossible at the moment until there is a standardized system to quickly and easily determine the app/window that has input focus on Wayland, just like we do so easily on X11.

Is keyszer compatible with Kinto.sh?

That is certainly the plan. The major reason Kinto.sh required it's own fork has been resolved. Kinto.sh should simply "just work" with keyszer (with a few tiny config changes). In fact, hopefully it works better than before since many quirks with the Kinto fork should be resolved. (such as nested combos not working, etc)

Reference:

How can I help or contribute?

Please open an issue to discuss how you'd like to get involved or respond on one of the existing issues. Also feel free to open new issues for feature requests. Many issues are tagged good first issue or help welcome.

License

keyszer is distributed under GPL3. See LICENSE.

keyszer's People

Contributors

17hao avatar amaya382 avatar drjaska avatar ellakk avatar gsuke avatar johnnymarnell avatar joshgoebel avatar jvasile avatar kevinslashslash avatar lenbok avatar mbachry avatar mooz avatar nlfiasel avatar nobuto-m avatar rbreaves avatar redbearak avatar smumriak avatar tangxinfa avatar terop avatar tieugene avatar watawuwu avatar wkwkw avatar ynjxsjmh avatar yuya373 avatar zhanghai avatar zw963 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

keyszer's Issues

Re: Insane numbers of combos for fixing numpad nav and media arrows

@joshgoebel

You should perhaps open an issue on my repo to discussion why this insane number of combos is needed and what might be done about that.

First posted in rbreaves/kinto#499

This is really something that is specific to the way Kinto currently remaps the modifier keys, plus the way GTK apps refuse to respect any Numpad nav keys, and then finally the Acer keyboard I have that places media functions on the Fn+arrow keys instead of PgUp/PgDn/Home/End. The PR for Linux is actually a combination of solutions for the two different problems I was having.

Now, one thing to understand is that all I've really done is remapped the shortcut combinations back to what they would have been had the user not been using Kinto. BUT, this is not necessarily correct. It just means that if those physical keys actually did something in Linux with Kinto disabled, they would continue to do whatever that is when Kinto is active. With the bonus that simple things like Shift+Numpad_PgUp actually works in GTK apps like a normal Shift+PgUp, and so on.

I'm interested in any ideas you have to consolidate this with some kind of generic "ignore" functionality that would cover all the combos. But with the shortcuts all laid out like this statically, someone can easily change just one of the specific remap groups to get their Numpad nav or media keys to do what they expect. If this was all lumped into some small algorithm, that would not be so easy.

Or, maybe you have some idea about a way to remap the base keys this is dealing with in such a way that all the individual remaps are no longer necessary. Get the Numpad nav keys to be recognized by everything regardless of any associated combo as the "real" nav keys you'd find on a full keyboard, between the QWERTY section and the Numpad.

Same with the arrow keys. If there could be a basic remap that just said, "Hey, these media functions are actually PgUp/PgDn/Home/End" that might solve the problem.

I hope we're on the same page with what the actual problem is that I'm solving by doing this. Funky GTK apps, and funky keyboards that don't put the right functions on the arrow keys. These remaps made both of those issues go away for me.

Collapse `multipurpose_modmap` into regular `modmap`

Update: Everywhere I say conditional what I really mean is multi-modmaps.

Right now multi modmaps are applied AFTER regular modmaps - creating a lot of complex code and the possibility of a double remap to happen... why? It seems that conditional modmaps (esp once we add suspend) are a lot more like regular modmaps... there keys are suspended when pressed and then held until either the timeout is reached (or another key is added, confirming that the modifier variant is necessary). So conditional modmaps invented suspend before I did... but it's based on a stored time, not a try async timer....

The system has no way to intervene 1 second later if you continue to hold the key... it can only take action if you press or release another key. This isn't quite correct... if the potential modifier is pressed and held past the limit then it should immediately assert itself as a modifier (and not the plain key)... it seems all of this could be brought under a single suspend system and hence a single modmap... so there would be no double mapping opportunity.... it might look like this:

modmap("default",
    Key.RIGHT_CTRL: Key.ESC,

    Key.ENTER: { "momentary": Key.ENTER, "held": Key.RIGHT_CTRL },
)

I use the dict style for clarity, we could also still allow the harder to read typle/array style if we so chose. So now at any given moment only a SINGLE conditional modmap would apply. Every key would either be mapped multipurpose or single purpose. Upon secondary input suspension would end.

Given the timeout settings it might be possible for a multi-purpose key suspension to timeout BEFORE other held keys suspensions timed out... if that happened I imagine the ambiguous momentary state could be marked as not having happened (ie the key is now a modifier for sure) and it could still remain suspended unto the other keys woke. Or it could immediately wake all keys.

I think this would be a vast simplification of what we have now and make things a lot more understandable.

Sequences of key combos are subject to noise from REAL inputs being re-exerted over and over

When I used Shift or Ctrl as part of the shortcut, your branch is failing in the same way as the Kinto branch when doing a Unicode macro. It exits the Unicode entry mode after the first character, then outputs the final three characters of the Unicode address. I'm suspecting that somehow the Unicode entry is getting interrupted by receiving a release event for Shift or Ctrl when I lift the associated physical shortcut keys.

Originally posted by @RedBearAK in #43 (comment)

Case for switching Kinto to Keyszer

Making an issue to track the list for a later post to the Kinto Github issues.

  • active project and active maintainer (using the keymapper daily)
  • simpler and more flexible configuration API
  • better debugging/logging (adde STOP hotkey, and DIAG hotkeys)
  • better multi-modmap support
  • potentially fixes false Alt/Super triggers #9
  • allows multi-combo nested keymaps to work once again
  • adds Command and Cmd aliases for Super/Meta
  • support Hyper as a modifier
  • support Fn as a modifier (on hardware where it works)
  • add_modifier config helper allows custom modifiers to be created
  • bind config helper supports sticky/bound keys for Mac OS style Cmd-tab, etc.
  • properly cleans up uinput pressed keys before termination
  • fixes issue where xmodmap cannot be used until some keys are pressed on the output
  • supports checking config file for issues with --check
  • avoids sending lots of wasted duplicate sync events to output
  • backwards compatible (easy upgrade)

Fast modifier+mouse clicks do not register (because of suspend)

Update: Of course this "just works" if you ALSO add your mouse as a device because the mouse buttons will trigger the modifiers to be un-suspended so that they can apply to the mouse button "keys".

If this works for everyone should this just be the solution? Finally a good reason to add add_device helper to the config file?


Output from start to trying to Ctrl+click a link. I held Ctrl for only a fraction of a second, which is how I frequently do it.

(--) CONFIG: /home/kris/.config/kinto/kinto.py
(+K) Grabbing AT Translated Set 2 keyboard (/dev/input/event4)
(--) Ready to process input.
(DD) modmap: LEFT_ALT => RIGHT_CTRL [define_conditional_modmap (old API)]
(DD) suspending keys [<Key.RIGHT_CTRL: 97>]
(DD) modmap: LEFT_ALT => RIGHT_CTRL [define_conditional_modmap (old API)]
(DD) modmap: LEFT_ALT => RIGHT_CTRL [define_conditional_modmap (old API)]
(DD) modmap: LEFT_ALT => RIGHT_CTRL [define_conditional_modmap (old API)]
(DD) modmap: LEFT_ALT => RIGHT_CTRL [define_conditional_modmap (old API)]
(DD) modmap: LEFT_ALT => RIGHT_CTRL [define_conditional_modmap (old API)]
(DD) resume because of mod release

You should join the Discord if that would be any easier.

Not a big fan of the more "chatty" type applications like Discord, but if that's easier for you...

Originally posted by @RedBearAK in #14 (comment)

Benchmark internals

Even something simple like this would be a huge help:

  • Load a large config (kinto is great!)
  • Make a global list of ALL key combos
  • Invent some random complex combos that aren't on any list

Now just run some different test suites, like say:

  • valid combos
  • invalid combos
  • a-z 0-9, ie just normal typing
  • non-key inputs
  • purposeful mix of all the above

And then you'd have a baseline performance metric to judge changes against. All the test would do is "type" the input as quickly as it could and send the output to a mocked output device which would just discard it - all we care about (here) is the time burnt inside transform.py.

@RedBearAK

Support FreeBSD and other BSDs

Creating a placeholder for this discussion. I'm not sure if this would be a welcome addition since another platform (esp. one I don't actually use) could significantly complicate supporting and developing the software - but perhaps if the patch was small enough... I really don't know enough about BSD's hardware input pieces to speculate.

I'm at least open to a discussion and seeing where this might go if someone was interested in doing the actual work/research.

Support Wayland

Basically we just need a way for Wayland to provide us:

  • currently focused application name
  • currently focused application window title #2
  • preferable a hooks or signals when either of these things change

If this is possible, hooking it up should be largely trivial and adding a way for a user to tell the keymapper if they are using X or Wayland. (perhaps we could even auto-detect based on ENV)

set_mark documentation

Ref: mooz/xkeysnail#97

I'm not sure what set_mark etc are supposed to do, I can make a PR for the README out of your answer here if that would help!

@jackmac92 If you'd be interested in making a PR here to document this I'd be happy to accept it. Also we have the wiki available though I haven't decided for sure how we should use it yet... as things stand now the README makes sense.

I plan on maintaining this fork (and seeking other contributors) since mooz seems to not be active anymore with xkeysnail.

Multi-purpose doesn't work well with fast use of shift.

Describe the bug or unexpected behavior

Multi-purpose doesn't work well with fast use of shift.

Sample Config or Instructions to Reproduce

multipurpose_modmap("default",
    {Key.BACKSLASH: [Key.BACKSLASH, LHyper]   # Enter2Cmd
})

If the keys happen in this order:

  • Press SHIFT
  • Press BACKSLASH
  • Release SHIFT
  • Release BACKSLASH

The release of SHIFT will trigger keys to unsuspend and because BACKLASH wasn't released immediately after pressing it will be converted to the LHyper modifier, which is incorrect.

Expected behavior

Shift-key should behave as it normally would, outputting the shifted version of the momentary key (not firing the modifier)

Alt/Super triggers other actions (wrongly) before combos are fired

Example: You press the combo Super-A (which does who knows what) but as soon as you press Super your Desktop Environments menu or control center pops up (as it normally does on hitting Super).

What you desire: for Super to be ignored IF it's being used as part of a remapped combo.


Update 2022-06-14

The current BIG problem here: Suspending ("waiting and seeing") doesn't seem compatible with Cmd-click... (to say open a link in a new tab, etc) For example you hit Cmd - and we wait to see what you do next (without passing the key to ouput - which would create a unintended keypress) - but then you click the mouse - which we don't have access to at all. So now you get just a plain click instead of a Cmd-click.

Now it's easy to just let the keymapper proxy the mouse as well (listening to both)... in this case we see Cmd, we wait, we see click, and the keymapper understands your trying to Cmd-click and it does that. This also isn't 100% optimal because it breaks things like your existing mouse configuration (accel, speed, etc)... and requires setting those carefully on both the INPUT mouse and the OUTPUT mouse. I'm currently doing this with a script, but it's annoying.

The larger problem I don't see how this can work with trackpads #20. So unless I'm missing something it seems either someone has:

  • no incorrect triggered keys, but cmd-click is broken
    • you have to hold down CMD a bit before you click
  • you're stuck with incorrect keys, but at least cmd-click works as normal

I'm not sure we want to try to support "all the above" just depending on how someone configures things, there is a lot of complexity in doing that.


Reference:

This should already be fixed but requires more testing (and perhaps knobs to tune)... I've added the concept of "suspended" keys... keys that you've pressed but (for a period of time) we aren't sure what to do with - because we don't know if you mean the literal key or if you're using them as part of a combo (that you're just beginning to key in).

Right now such keys are "suspended" for 1 second. After that (if a combo hasn't been triggers) any keys you've pressed will be un-suspended and pressed on the output. If a combo is triggered the suspension is extended (to allow additional combos).

This seems to work well the only thing it would break is any type of "press and hold" modifier functionality like if you wanted to press and hold Super (and have it pop up some sort of menu/widget) immediately... that can't happen because we're suspending the Super key for 1 second. We may need tuning knobs to modify the delay here.

Press and release works immediately... as soon as it sees you start releasing keys the output is resumed and the presses (and releases) will be communicated to the output.

  • further testing of interaction with multipurpose-modmaps

Rip out `launch` that assumes root or current user context?

  • Does launch make sense when running as a privileged (non-root) user?
  • How would you run commands as the current user?
  • Or does it only make sense when running yourself as a slightly elevated-user with access to input devices?
  • Should we even support this latter config (running as the current user) as it could allow malicious software to easily log all keystrokes?

Allow definition of custom modifiers vs the current hard-coded set

  • See #7, this work is largely finished.

Currently modifiers are treated quite differently than normal keys. As such you can use non-modifiers to trigger non-combos: IE, you can modmap Ctrl -> ESC, so that you can use Ctrl to trigger combos that would typically require ESC.

But you cannot do ESC-x in a keymap, because ESC is a "normal" key, it's not a modifier and only modifiers maybe used (as modifiers) in keymaps.

One of two things should happen here:

ONE. The distinction should be removed entirely. (letting the software decide what is a modifier and what isn't - why should we care)

This might be hard to achieve in practice though as we make a lot of things easier by the current way we divide keys vs modifiers.

TWO. One should be allowed to conditionally (or unconditionally) specify additional keys to use as modifiers. IE:

allow_as_modifiers([Key.ESC, Key.CAPS_LOCK])

Support "binding" press and hold for some combos, like Cmd-tabbing

Currently combos are seen as "discrete units" in that you can easily do the following on the output:

  • Remap CMD-tab to Ctrl-Tab
  • CMD-tab
  • CMD-tab
  • CMD-tab

This will Ctrl-Tab 3 times (fully pressing AND releasing all the keys every time). What you cannot do is:

  • Remap CMD-tab to Ctrl-Tab
  • Press and hold CMD (to hold a switcher open)
  • Hit tab (while CMD is steadily down)
  • Hit tab (while CMD is steadily down)
  • Hit tab (while CMD is steadily down)

The "and hold" part is impossible with xkeysnail 0.4 and is the reason Kinto.sh currently uses a patched xkeysnail (that has it's own issues).


The plan is to fix this "better" by implementing smarter sticky keys such that the keymapper understands when you might be triggering a "sticky" combo and holds the key down on the output until you are finished.

Name of timeout settings

@RedBearAK thoughts?

I'm thinking:

  • multipurpose
  • suspend
  • exert_post_combo

And yes we would document these and their uses...

timeouts(
  multipurpose = 1,
  suspend = 0.1,
  exert_post_combo: 0.25
)

The old define_timeout API would set the multipurpose timeout, as it always has. The exert_post_combo I don't have yet but if I add it later it would prevent false keypresses AFTER a combo by adding a small delay there (to give the user a chance to lift the keys) before the keys were reexerted on the output (currently they are reexerted immediately). That seems far less risky to me than the suspend which is the delay that happens a mod is first held - and what breaks your Cmd-clicking.

Support conditionality based on `WM_NAME`

Support keymap and modmaps that are conditional on window name, not just class.


What needs to be done IMHO:

  • Support for wm_name added to KeyContext
  • Tests for wm_name conditions (at least in keymap basics)
  • Need test helper (for setting window context) for differentiating wm_class from wm_name (since we have a single window helper now
  • xorg likely needs to have a function that returns a dict with both name and class that KeyContext can consume

Related: tridactyl/tridactyl#4256

I dislike how this is speced in mooz/xkeysnail#129 :

define_keymap(lambda wm_class, device_name, wm_name: "Google Docs" in wm_name, {
     K("M-x"): K("M-slash")
})

This "just add another param to lambda" isn't great to read and is hard to deal with on the other side due to the varying number of arguments. (not to mention remembering the ordering, etc).


So I definitely think we should support this, it's just a question of what syntax to use... I'm perhaps imagining a wrapper function, but open to other suggestions.

# allow an array of conditions...
conditional([lambda wm_name: "Google Docs" in wm_name], 
   keymap("Google Docs", {
       K("M-x"): K("M-slash")
   })
)

Or even better a single context dict:

conditional(lambda ctx: "Google Docs" in ctx.wm_name , 
   keymap("Google Docs", {
       K("M-x"): K("M-slash")
   })
)

So instead of modmap and conditional_modmap there would only be a single modmap API that you would either wrap in a conditional or not...

This work has already been done now.

`masquerade_as` option to allow output device to resemble input

Sometimes the device details matter - like when libinput/Gnome is deciding if a keyboard/trackpad are "paired" for it's "ignore trackpad when typing" option... the virtual keyboard we present breaks this since it doesn't look like an "internal" paired keyboard... this can be fixed if we "masquerade" as the internal keyboard by copying all it's details and making our virtual keyboard look as similar as possible.

It seems reasonable to allow someone to do this in the config file. Proposed:

masquerade_as "AT Translated Set 2 keyboard"

The name would be the name of the device in --list-devices.


Original Title: Not respect GNOME touchpad disable-while-typing settings

This issue come from original xkeysnail discuss.

mooz/xkeysnail#144 (comment)

Maybe there exist some improvement, let us discuss here.

Thank you.

After a combo should we resuspend keys (even if they had already been exerted on output)?

Please also read issue #9 for the full context here.


Look at this test:

async def test_after_combo_should_lift_exerted_keys():
    keymap("Firefox",{
        K("Ctrl-j"): K("Alt-TAB"),
    })

    boot_config()

    press(Key.LEFT_CTRL)
    await asyncio.sleep(2)
    press(Key.J)
    release(Key.J)
    release(Key.LEFT_CTRL)

    assert _out.keys() == [
        (PRESS, Key.LEFT_CTRL),
        # beginning of combo will release left ctrl since it's not part of combo
        (RELEASE, Key.LEFT_CTRL),
        (PRESS, Key.LEFT_ALT),
        (PRESS, Key.TAB),
        (RELEASE, Key.TAB),
        (RELEASE, Key.LEFT_ALT),
        # but now we reassert left ctrl
        (PRESS, Key.LEFT_CTRL),
        # and finally we release it
        (RELEASE, Key.LEFT_CTRL),
    ]

We hold CTRL a long time, so long that we have to push it to the output... then finally we end up using it in a combo (that it's not part of)... this means we have to temporarily lift it from the output (to send the combo)... then after the combo is sent we immediately re-exert Ctrl on output since it's still held down.

That is the current behavior... but since we just "spent" the modifier on the combo... should we instead decide to suspend it after the combo has completed? IE, put it back in the "we don't know what to do with it yet, lets wait and see" queue... until the timeout again fires and we have to resume output...

If we did this then the above test would change such that you never saw the last two events (assuming Ctrl was released after the combo)...

What happens if multipurpose modmap is resumed before another key?

After 1 second (or configurable delay) the multi-purpose key will wake up and try to resolve itself... if no other keys have been pressed (and it hasn't been released) then it will resolve as a modifier... but if it happens to be a regular key we can't exert it on the output - of that will just send the key to the output...

So right now this is buggy... I'm beginning to wonder if for non-modifier multi-purpose modifiers (say using Enter or ; as a multi-purposed mod that we shouldn't just suspend indefinitely until a second key is hit (or the first is released).

(docs/usability) Key.SUPER vs Key.META

Ref: mooz/xkeysnail#158

The names in key.py come to us from the kernel, so I'm hesitant to change them but perhaps we should add an alias:

LEFT_SUPER = LEFT_META
RIGHT_SUPER = RIGHT_META

And then encourage the use of these over the more ambiguous META naming?

Procedure for testing/comparing this branch vs others

@joshgoebel

Readme says the installation instructions aren't correct yet. I just want to establish the best way to quickly go back and forth between this branch and (in my case) the Kinto branch.

When I was messing with the mainline branch all I had to do was sudo pip3 install --upgrade xkeysnail, which would pull down the normal xkeysnail and replace the patched Kinto version. Then I would just reinstall Kinto to revert back to the "held keys" patched version and get everything back to normal.

But, this is a destructive process. Not only does the Kinto install overwrite any customized config file with the default, but on my main system I'm running xkeysnail as user rather than root, and the Kinto installer would attempt to set up xkeysnail to run from the systemd service file that runs xkeysnail as root, which I would have to disable each time.

So I want to use this thread to record the commands needed to download and install this branch, and then easily download and reinstall the Kinto patched branch without re-running the entire Kinto installer. Right now I'm not exactly sure how to do that, but I should be able to pull the right command out of the setup.py file to pull the right branch down and install it.

Will add to this later.

Launching apps using a separate client running as active user

This is trivial to add to existing configs by using a named pipe for one-way communication from server to client. The named pipe just needs to be located somewhere that your active user has access to.

First, make a FIFO that we can use for comms and set it's permissions properly:

sudo mkfifo -m644 /var/run/keyszer-launcher-sub
chown keymapper:keymapper /var/run/keyszer-launcher-sub

Then our config file might look something like this:

import os
LAUNCH_PIPE = "/var/run/keyszer-launcher-sub"

fd = os.open(LAUNCH_PIPE, os.O_RDWR)
launch_fifo = os.fdopen(fd, "w")

def launch(command):
    def launcher():
        launch_fifo.write(f"LAUNCH {command}")
        launch_fifo.flush()
    return launcher

# keymaps

keymap("default", {
  K("Cmd-Plus"): launch("raise-volume"),
  K("Cmd-Minus"): launch("lower-volume"),
})

And a simple Bash launch client:

#!usr/bin/env bash
file="/var/run/keyszer-launcher-sub"

while read -r line; do
    words=( $line )
    case ${words[0]} in
    LAUNCH)
      unset 'words[0]'
      cmd=${words[@]}
      case $cmd in
        raise-volume)
          pulsemixer --change-volume +5
          ;;
        lower-volume)
          pulsemixer --change-volume -5
          ;;
      esac
      ;;
    *)
      echo "Dunno what to do with: ${words[@]}"
      ;;
    esac

    #echo -e "$line\n"
done <$file 

Need some match helpers now to avoid ugliness like this:

  • figure out good helper names (discuss here)
  • add helpers to config_api
  • write tests

Need some match helpers now to avoid ugliness like this:

    conditional(lambda ctx: re.compile("Firefox").search(ctx.wm_class),
        keymap("Firefox",{
            K("C-M-j"): K("C-TAB"),
            K("C-M-k"): K("C-Shift-TAB"),
        })
    )

Since now the requirement is we always pass a function, we may need a few helpers to tidy this up.

IE:

conditional( wm_class_matches_regex("Firefox"), # ...

Or something... We can't just pass raw regex around anymore because the target isn't clear... device name? wm class? wm name (soon)? other?

Originally posted by @joshgoebel in #3 (comment)

Task switcher getting stuck open or failing to appear

@joshgoebel

Just loaded up the latest keyszer and I'm seeing something I haven't really seen before. If I do Cmd+Shift+Tab, the task switcher won't even show up. And the switch is immediate.

If I start with Cmd+Tab and then do Cmd+Shift+Tab, the task switcher gets stuck open on screen until I hit Enter or tap the "Option" (Super/Win remapped to Alt on PC) key. Tapping the other modifiers doesn't do anything.

Don't recall seeing either of these behaviors on previous iterations of keyszer.

(Looks like you're not getting automatically added to new issues in this repo? Says "No one assigned" in the sidebar.)

installation failed in voidlinux

Hi,

@joshgoebel Thanks for notifying (mooz/xkeysnail#92 (comment)). Tried today to install in voidlinux with python 3.10, but landed with the following error. No logfile named `/tmp/tmpgr5y1mxf did exist.

$ pip3 install --user --upgrade .
Processing /home/zenny/Downloads/gitrepos/keyszer-xkeysnail-joshgoebel
  Installing build dependencies ... done
  Getting requirements to build wheel ... done
  Preparing wheel metadata (pyproject.toml) ... error
  ERROR: Command errored out with exit status 1:
   command: /usr/bin/python3 /home/zenny/.local/lib/python3.10/site-packages/pip/_vendor/pep517/in_process/_in_process.py prepare_metadata_for_build_wheel /tmp/tmpgr5y1mxf
       cwd: /home/zenny/Downloads/gitrepos/keyszer-xkeysnail-joshgoebel
  Complete output (20 lines):
  Traceback (most recent call last):
    File "/home/zenny/.local/lib/python3.10/site-packages/pip/_vendor/pep517/in_process/_in_process.py", line 349, in <module>
      main()
    File "/home/zenny/.local/lib/python3.10/site-packages/pip/_vendor/pep517/in_process/_in_process.py", line 331, in main
      json_out['return_val'] = hook(**hook_input['kwargs'])
    File "/home/zenny/.local/lib/python3.10/site-packages/pip/_vendor/pep517/in_process/_in_process.py", line 147, in prepare_metadata_for_build_wheel
      whl_basename = backend.build_wheel(metadata_directory, config_settings)
    File "/tmp/pip-build-env-jqoubx0o/overlay/lib/python3.10/site-packages/hatchling/build.py", line 41, in build_wheel
      return os.path.basename(next(builder.build(wheel_directory, ['standard'])))
    File "/tmp/pip-build-env-jqoubx0o/overlay/lib/python3.10/site-packages/hatchling/builders/plugin/interface.py", line 81, in build
      self.metadata.validate_fields()
    File "/tmp/pip-build-env-jqoubx0o/overlay/lib/python3.10/site-packages/hatchling/metadata/core.py", line 163, in validate_fields
      _ = self.version
    File "/tmp/pip-build-env-jqoubx0o/overlay/lib/python3.10/site-packages/hatchling/metadata/core.py", line 52, in version
      self._set_version()
    File "/tmp/pip-build-env-jqoubx0o/overlay/lib/python3.10/site-packages/hatchling/metadata/core.py", line 145, in _set_version
      version = self.hatch.version.cached
    File "/tmp/pip-build-env-jqoubx0o/overlay/lib/python3.10/site-packages/hatchling/metadata/core.py", line 1181, in cached
      raise type(e)(f'Error getting the version from source `{self.source.PLUGIN_NAME}`: {e}') from None
  OSError: Error getting the version from source `regex`: file does not exist: src/keyszer/info.py
  ----------------------------------------
WARNING: Discarding file:///home/zenny/Downloads/gitrepos/keyszer-xkeysnail-joshgoebel. Command errored out with exit status 1: /usr/bin/python3
/home/zenny/.local/lib/python3.10/site-packages/pip/_vendor/pep517/in_process/_in_process.py prepare_metadata_for_build_wheel /tmp/tmpgr5y1mxf Check the logs for full command output.
ERROR: Command errored out with exit status 1: /usr/bin/python3 /home/zenny/.local/lib/python3.10/site-packages/pip/_vendor/pep517/in_process/_in_process.py prepare_metadata_for_build_wheel /tmp/tmpgr5y1mxf Check the logs for full command output.

Cheers.

Feature: Support HYPER

  • add a add_modifier helper to config_api for use in config files to define new modifiers

Usage example:

add_modifier("HYPER", aliases = ["Hyper"], key = Key.F24) 

Or to declare a pair:

add_modifier("R_SUPER", aliases = ["RSuper", "RWin"], key = Key.RIGHT_META)
add_modifier("L_SUPER", aliases = ["LSuper", "LWin"], key = Key.LEFT_META)
add_modifier("SUPER", aliases = ["Super", "Win"], keys = [Key.LEFT_META, Key.RIGHT_META])

Related:

We should support this (somehow), but the problem is that Hyper isn't a "real" key... https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h

At the moment modifiers must (before any remapping) be hard-mapped to REAL keyboard keys: https://github.com/joshgoebel/xkeysnail/blob/main/xkeysnail/key.py#L755

So as the code currently stands to support them we'd have to hard bind them to two actual real keyboard keys. In the future I'd like to allow arbitrary modifiers, but right now modifiers are very particular (and handled differently than other keys).


So... back to real keys... the problem is there aren't any [for hyper] - I'm not sure how I feel about just steal from the unused keycodes (249-254) [as has been suggested]... and I'm not sure I'm aware of any convention about how hyper is mapped (to real keys)... aliasing is a possibility but which keycodes can we guarantee no one is using?

New functions for easier macro strings and Unicode?

Macros with lengthy strings are a huge pain to implement currently due to the fact that even the simplest of strings needs to be broken down by the user into individual "keystrokes" inside K function constructs. Shortcut combos are one thing, but when there is just a regular string it seems like there should be a way to make things a lot easier on the user by offering a function that just takes whole strings, without forcing the user to pre-digest them. A new function (maybe S for string) could just take all the literal characters within the quotes and send them one by one to output (through any necessary limiter for output reliability, of course).

So instead of doing this kind of nonsense:

    K("C-comma"): [
        K("C-T"),K("a"),K("b"),K("o"),K("u"),K("t"),
        K("Shift-SEMICOLON"),K("p"),K("r"),K("e"),K("f"),
        K("e"),K("r"),K("e"),K("n"),K("c"),K("e"),K("s"),K("Enter")
    ],

The user could instead just do something like:

    K("C-comma"): [ K("C-t"), S("about:config"), K("Enter") ],

I wasn't even really thinking about this while I was drafting this but of course this would make it hugely easier to do the Unicode stuff.

    K("Alt-Key_1"): [
        K("Shift-Ctrl-u"), S("00A1"), K("Enter")
    ],

But if I had my preference, before I embark on implementing a couple hundred shortcuts for the Option key special characters I would vastly prefer a dedicated Unicode sending function to encapsulate all that into a simpler form, like...

# Inverted Question Mark
K("Alt-Key_1"): U("U+00A1"),
# Dead Keys: Grave Accent
K("Alt-Grave"): { 
    K("a"): U("U+00E0"),
    K("e"): U("U+00E8"),
}

Stuck keys troubleshooting

@joshgoebel

f8165f5
And try tuning the delay (manually) from 1s to lower and see if that helps... tranform line 200 or so... call_later.

Finally got a chance to do a fresh clone and try this. I set it to 0.2 and it seems to work a lot better. If I'm particularly quick it's still not quite fast enough to do what I expect. So I set it to 0.1.

It's safe to say I'm now confused a bit why the suspension time is even necessary if it can be set to such a short delay.

And still having issues with stuck modifiers. Seems to happen pretty reliably after using something like Cmd+Tab then moving to Cmd+Shift+Grave. Once the new window pops up, there is definitely at least one stuck modifier. I think it may be multiple keys because tapping just Alt (physical Super/Win) or RC (physical Alt) doesn't completely stop unexpected actions from occurring.

(enh) Detect/Respect NumLock state for Numpad keys

As discussed in mooz/xkeysnail issue 132, I'm hoping this fork could add the ability to use numpad keys while respecting the NumLock state, so that some fancy tricks could be done like forcing the numpad to always act like a numpad, or remapping the numpad "navigation" functions separately from the NumLock ON number functions.

mooz/xkeysnail#132

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.