├── .envrc ├── .github ├── FUNDING.yml └── workflows │ └── CI.yml ├── .gitignore ├── .gitmodules ├── .idea ├── codeStyles │ └── codeStyleConfig.xml ├── inspectionProfiles │ └── Project_Default.xml ├── map2.iml ├── modules.xml └── vcs.xml ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── PKGBUILD ├── README.md ├── ci ├── generate-pkgbuild.py ├── prepare-ci-container.sh ├── prepare-ci-test.sh └── templates │ └── PKGBUILD ├── docs-old ├── man │ └── map2.1 └── start-automatically.md ├── docs ├── .gitignore ├── README.md ├── astro.config.mjs ├── package.json ├── prettier.config.cjs ├── public │ ├── default-og-image.png │ ├── favicon.ico │ ├── favicon.svg │ ├── fonts │ │ ├── ibm-plex-mono-v15-latin-italic.woff │ │ ├── ibm-plex-mono-v15-latin-italic.woff2 │ │ ├── ibm-plex-mono-v15-latin-regular.woff │ │ └── ibm-plex-mono-v15-latin-regular.woff2 │ ├── logo.png │ ├── logo.svg │ └── make-scrollable-code-focusable.js ├── renovate.json ├── sandbox.config.json ├── src │ ├── components │ │ ├── Footer │ │ │ ├── AvatarList.astro │ │ │ └── Footer.astro │ │ ├── HeadCommon.astro │ │ ├── HeadSEO.astro │ │ ├── Header │ │ │ ├── AstroLogo.astro │ │ │ ├── Header.astro │ │ │ ├── LanguageSelect.tsx │ │ │ ├── Search.tsx │ │ │ ├── SidebarToggle.tsx │ │ │ └── SkipToContent.astro │ │ ├── LeftSidebar │ │ │ └── LeftSidebar.astro │ │ ├── PageContent │ │ │ └── PageContent.astro │ │ ├── RightSidebar │ │ │ ├── Example.solid.tsx │ │ │ ├── MoreMenu.astro │ │ │ ├── RightSidebar.astro │ │ │ ├── TableOfContents.tsx │ │ │ ├── ThemeToggleButton.scss │ │ │ └── ThemeToggleButton.tsx │ │ └── ValidKeysTable.solid.tsx │ ├── consts.ts │ ├── content │ │ ├── config.ts │ │ └── docs │ │ │ └── en │ │ │ ├── advanced │ │ │ ├── autostart.mdx │ │ │ └── secure-setup.mdx │ │ │ ├── api │ │ │ ├── chord-mapper.mdx │ │ │ ├── map2.mdx │ │ │ ├── mapper.mdx │ │ │ ├── reader.mdx │ │ │ ├── text-mapper.mdx │ │ │ ├── virtual-writer.mdx │ │ │ ├── window.mdx │ │ │ └── writer.mdx │ │ │ ├── basics │ │ │ ├── getting-started.mdx │ │ │ ├── install.mdx │ │ │ ├── introduction.mdx │ │ │ ├── keys-and-key-sequences.mdx │ │ │ └── routing.mdx │ │ │ └── examples │ │ │ ├── chords.mdx │ │ │ ├── hello-world.mdx │ │ │ ├── keyboard-to-controller.mdx │ │ │ ├── text-mapping.mdx │ │ │ └── wasd-mouse-control.mdx │ ├── env.d.ts │ ├── languages.ts │ ├── layouts │ │ └── MainLayout.astro │ ├── pages │ │ ├── [...slug].astro │ │ └── index.astro │ └── styles │ │ ├── index.scss │ │ ├── langSelect.scss │ │ ├── search.scss │ │ └── theme.scss ├── tsconfig.json └── yarn.lock ├── evdev-rs ├── .gitignore ├── .travis.yml ├── Cargo.toml ├── README.md ├── TODO.md ├── evdev-sys │ ├── Cargo.toml │ ├── build.rs │ └── src │ │ └── lib.rs ├── examples │ └── evtest.rs ├── rustfmt.toml ├── src │ ├── device.rs │ ├── enums.rs │ ├── lib.rs │ ├── logging.rs │ ├── macros.rs │ ├── uinput.rs │ └── util.rs ├── tests │ └── all.rs └── tools │ ├── make-enums.sh │ └── make-event-names.py ├── examples ├── README.md ├── a_to_b.py ├── active_window.py ├── chords.py ├── hello_world.py ├── keyboard_to_controller.py ├── tests │ ├── _setup_integration_tests │ │ └── setup_integration_tests.rs │ ├── a_to_b.rs │ ├── chords.rs │ ├── hello_world.rs │ ├── text_mapping.rs │ └── wasd_mouse_control.rs ├── text_mapping.py └── wasd_mouse_control.py ├── package.sh ├── pytests ├── Cargo.lock ├── Cargo.toml └── src │ └── lib.rs ├── release.sh ├── rustfmt.toml ├── scripts └── arch │ ├── .SRCINFO │ └── PKGBUILD ├── shell.nix ├── src ├── capabilities │ └── mod.rs ├── closure_channel.rs ├── device │ ├── device_logging.rs │ ├── mod.rs │ ├── virt_device.rs │ ├── virtual_input_device.rs │ └── virtual_output_device.rs ├── encoding.rs ├── error.rs ├── event.rs ├── event_handlers.rs ├── event_loop.rs ├── evlist │ └── evlist.rs ├── global.rs ├── key_defs.rs ├── key_primitives.rs ├── lib.rs ├── logging.rs ├── man │ └── man.rs ├── mapper │ ├── chord_mapper.rs │ ├── mapper.rs │ ├── mapper_util.rs │ ├── mapping_functions.rs │ ├── mod.rs │ ├── suffix_tree.rs │ └── text_mapper.rs ├── parsing │ ├── action_state.rs │ ├── custom_combinators.rs │ ├── error.rs │ ├── identifier.rs │ ├── key.rs │ ├── key_action.rs │ ├── key_sequence.rs │ ├── mod.rs │ ├── motion_action.rs │ └── public_parsing_api.rs ├── platform │ └── mod.rs ├── python.rs ├── reader.rs ├── subscriber.rs ├── testing │ └── mod.rs ├── virtual_writer.rs ├── window │ ├── hyprland_window.rs │ ├── mod.rs │ ├── window_base.rs │ └── x11_window.rs ├── writer.rs ├── xkb.rs └── xkb_transformer_registry.rs └── test.sh /.envrc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # the shebang is ignored, but nice for editors 3 | 4 | if type -P lorri &>/dev/null; then 5 | eval "$(lorri direnv)" 6 | else 7 | echo 'while direnv evaluated .envrc, could not find the command "lorri" [https://github.com/nix-community/lorri]' 8 | use nix 9 | fi 10 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: "shiroi_usagi" 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # ["https://ko-fi.com/shiroi_usagi"] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/workspace.xml 2 | 3 | /local 4 | /target 5 | pytests/target 6 | /pkg 7 | /venv 8 | *.zst 9 | *.tar.gz 10 | /dist 11 | .vim/ 12 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "evdev-rs/evdev-sys/libevdev"] 2 | path = evdev-rs/evdev-sys/libevdev 3 | url = https://gitlab.freedesktop.org/libevdev/libevdev.git 4 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [2.1.1] - 2024-08-08 11 | 12 | ### Added 13 | 14 | - Active window change handler now gets called when created (Hyprland) 15 | 16 | ## [2.0.19] - 2024-06-04 17 | 18 | ### Added 19 | 20 | - We now build aarch64 wheels as well! 21 | - Improved e2e tests 22 | - Stabilized text mapper 23 | - Stabilized chord mapper 24 | - Documentation for text and chord mappers 25 | - New changelog 26 | - Github actions CI for release notes 27 | 28 | ### Fixed 29 | 30 | - Text mapper bugs 31 | - Documentation cleanup 32 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "map2" 3 | version = "2.1.1" 4 | authors = ["shiro "] 5 | edition = "2021" 6 | 7 | [features] 8 | integration = [] 9 | extension-module = ["pyo3/extension-module"] 10 | default = ["extension-module"] 11 | 12 | [lib] 13 | name = "map2" 14 | crate-type = ["lib", "cdylib"] 15 | 16 | [dependencies] 17 | anyhow = "1.0" 18 | clap = "4.5.3" 19 | evdev-rs = { path = "evdev-rs", features = ["serde"] } 20 | udev = "0.9.0" 21 | futures = "0.3.30" 22 | futures-time = "3.0.0" 23 | input-linux-sys = "0.3.1" 24 | itertools = "0.13.0" 25 | lazy_static = "1.4.0" 26 | libc = "0.2.150" 27 | arc-swap = "1.7.0" 28 | man = "0.3.0" 29 | nom = "7.1.3" 30 | notify = "4.0.16" 31 | regex = "1.10.2" 32 | tap = "1.0.1" 33 | tokio = { version = "1.13.0", features = ["full"] } 34 | unicode-xid = "0.2.4" 35 | walkdir = "2.4.0" 36 | x11rb = "0.7.0" 37 | # hyprland = "0.4.0-alpha.3" 38 | hyprland = { git = "https://github.com/hyprland-community/hyprland-rs.git", rev = "refs/pull/177/head" } 39 | xdg = "2.2.0" 40 | atty = "0.2" 41 | indoc = "1.0" 42 | futures-intrusive = "0.4.0" 43 | pyo3 = { version = "0.20.3" } 44 | pyo3-asyncio = { version = "0.20.0", features = [ 45 | "attributes", 46 | "tokio-runtime", 47 | "testing", 48 | ] } 49 | oneshot = "0.1.6" 50 | signal-hook = "0.3.17" 51 | uuid = { version = "1.5.0", features = ["v4"] } 52 | bitflags = "1.3.2" 53 | byteorder = "1.5.0" 54 | tempfile = "3.8.1" 55 | nix = "0.26.4" 56 | thiserror = "1.0.50" 57 | serde = { version = "1.0.192", features = ["derive"] } 58 | serde_json = { version = "1.0.108" } 59 | pythonize = { version = "0.20.0" } 60 | 61 | # wayland 62 | wayland-client = "0.31.1" 63 | wayland-protocols-misc = { version = "0.2.0", features = [ 64 | "client", 65 | "wayland-server", 66 | "wayland-client", 67 | ] } 68 | xkeysym = "0.2.0" 69 | unicode-segmentation = "1.10.1" 70 | xkbcommon = { version = "0.7.0", features = ["wayland"] } 71 | 72 | [dev-dependencies] 73 | automod = "1.0.13" 74 | pytests = { path = "./pytests" } 75 | 76 | [[test]] 77 | name = "integration-tests" 78 | path = "examples/tests/_setup_integration_tests/setup_integration_tests.rs" 79 | harness = false 80 | required-features = ["integration"] 81 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 shiro 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: shiro 2 | 3 | pkgname=map2 4 | pkgver=1.0.6 5 | pkgrel=1 6 | pkgdesc="A scripting language that allows complex key remapping on Linux, written in Rust" 7 | arch=('x86_64' 'i686') 8 | license=('MIT') 9 | depends=() 10 | makedepends=(rustup) 11 | 12 | build() { 13 | cd .. 14 | cargo build --release --locked --all-features --target-dir=target 15 | } 16 | 17 | check() { 18 | cd .. 19 | cargo test --release --locked --target-dir=target 20 | } 21 | 22 | package() { 23 | cd .. 24 | install -Dm 755 target/release/${pkgname} -t "${pkgdir}/usr/bin" 25 | 26 | install -Dm644 docs/man/map2.1 "$pkgdir/usr/share/man/man1/map2.1" 27 | } 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

map2

3 |

Linux input remapping
Remap your keyboard, mouse, controller and more!

4 | 5 | [![GitHub](https://img.shields.io/badge/GitHub-code-blue?logo=github)](https://github.com/shiro/map2) 6 | [![MIT License](https://img.shields.io/github/license/shiro/map2?color=43A047&logo=linux&logoColor=white)](https://github.com/shiro/map2/blob/master/LICENSE) 7 | [![Discord](https://img.shields.io/discord/1178929723208896543?label=Discord&color=5E35B1&logo=discord&logoColor=ffffff)](https://discord.gg/brKgH43XQN) 8 | [![Build](https://img.shields.io/github/actions/workflow/status/shiro/map2/CI.yml?color=00897B&logo=github-actions&logoColor=white)](https://github.com/shiro/map2/actions/workflows/CI.yml) 9 | [![Donate](https://img.shields.io/badge/Ko--Fi-donate-orange?logo=ko-fi&color=E53935)](https://ko-fi.com/C0C3RTCCI) 10 |
11 | 12 | Want to remap your input devices like keyboards, mice, controllers and more? 13 | There's nothing you can't remap with **map2**! 14 | 15 | - 🖱️ **Remap keys, mouse events, controllers, pedals, and more!** 16 | - 🔧 **Highly configurable**, using Python 17 | - 🚀 **Blazingly fast**, written in Rust 18 | - 📦 **Tiny install size** (around 5Mb), almost no dependencies 19 | - ❤️ **Open source**, made with love 20 | 21 | Visit our [official documentation](https://shiro.github.io/map2/en/basics/introduction) 22 | for the full feature list and API. 23 | 24 | --- 25 | 26 |
27 | If you like open source, consider supporting 28 |
29 |
30 | Buy Me a Coffee at ko-fi.com 31 |
32 | 33 | ## Install 34 | 35 | The easiest way is to use `pip`: 36 | 37 | ```bash 38 | pip install map2 39 | ``` 40 | 41 | For more, check out the [Install documentation](https://shiro.github.io/map2/en/basics/install/). 42 | 43 | After installing, please read the 44 | [Getting started documentation](https://shiro.github.io/map2/en/basics/getting-started). 45 | 46 | ## Example 47 | 48 | ```python 49 | import map2 50 | 51 | # readers intercept all keyboard inputs and forward them 52 | reader = map2.Reader(patterns=["/dev/input/by-id/my-keyboard"]) 53 | # mappers change inputs, you can also chain multiple mappers! 54 | mapper = map2.Mapper() 55 | # writers create new virtual devices we can write into 56 | writer = map2.Writer(clone_from = "/dev/input/by-id/my-keyboard") 57 | # finally, link nodes to control the event flow 58 | map2.link([reader, mapper, writer]) 59 | 60 | # map the "a" key to "B" 61 | mapper.map("a", "B") 62 | 63 | # map "CTRL + ALT + u" to "META + SHIFT + w" 64 | mapper.map("^!u", "#+w") 65 | 66 | # key sequences are also supported 67 | mapper.map("s", "hello world!") 68 | 69 | # use the full power of Python using functions 70 | def custom_function(key, state): 71 | print("called custom function") 72 | 73 | # custom conditions and complex sequences 74 | if key == "d": 75 | return "{ctrl down}a{ctrl up}" 76 | return True 77 | 78 | mapper.map("d", custom_function) 79 | ``` 80 | 81 | ## Build from source 82 | 83 | To build from source, make sure python and rust are installed. 84 | 85 | ```bash 86 | # create a python virtual environment 87 | python -m venv .env 88 | source .env/bin/activate 89 | 90 | # build the library 91 | maturin develop 92 | ``` 93 | 94 | While the virtual environment is activated, all scripts ran from this terminal 95 | will use the newly built version of map2. 96 | 97 | 98 | ## Contributing 99 | 100 | If you want to report bugs, add suggestions or help out with development please 101 | check the [Discord channel](https://discord.gg/brKgH43XQN) and the [issues page](https://github.com/shiro/map2/issues) and open an issue 102 | if it doesn't exist yet. 103 | 104 | ## License 105 | 106 | MIT 107 | 108 | ## Authors 109 | 110 | - shiro 111 | -------------------------------------------------------------------------------- /ci/generate-pkgbuild.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from os import environ, makedirs 3 | import hashlib 4 | 5 | 6 | release_tag = environ.get("RELEASE_TAG") 7 | if not release_tag: 8 | print("::error ::RELEASE_TAG is required but missing") 9 | exit(1) 10 | 11 | 12 | def calculate_sha256(filename): 13 | sha256_hash = hashlib.sha256() 14 | with open(filename, "rb") as f: 15 | # read and update hash string value in blocks of 4K 16 | for byte_block in iter(lambda: f.read(4096), b""): 17 | sha256_hash.update(byte_block) 18 | return sha256_hash.hexdigest() 19 | 20 | 21 | print("Generating PKGBUILD for map2...") 22 | makedirs("./dist/aur", exist_ok=True) 23 | with open("./dist/aur/PKGBUILD", "w") as out: 24 | checksum_x86_64 = calculate_sha256(f"./wheels/map2-{release_tag}-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl") 25 | checksum_i686 = calculate_sha256(f"./wheels/map2-{release_tag}-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl" ) 26 | 27 | content = open("./ci/templates/PKGBUILD").read()\ 28 | .replace("pkgver=", f"pkgver={release_tag}")\ 29 | .replace("sha256sums_x86_64=('')", f"sha256sums_x86_64=('{checksum_x86_64}')")\ 30 | .replace("sha256sums_i686=('')", f"sha256sums_i686=('{checksum_i686}')") 31 | 32 | out.write(content) 33 | -------------------------------------------------------------------------------- /ci/prepare-ci-container.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | if command -v apt-get &> /dev/null; then 6 | echo "using apt-get" 7 | apt-get update 8 | apt-get install -y libxkbcommon0 libxkbcommon-dev libxkbcommon-tools automake libtool pkg-config 9 | 10 | cat <> /etc/apt/sources.list 11 | deb [arch=arm64] http://ports.ubuntu.com/ jammy main multiverse universe 12 | deb [arch=arm64] http://ports.ubuntu.com/ jammy-security main multiverse universe 13 | deb [arch=arm64] http://ports.ubuntu.com/ jammy-backports main multiverse universe 14 | deb [arch=arm64] http://ports.ubuntu.com/ jammy-updates main multiverse universe 15 | EOF 16 | dpkg --add-architecture arm64 17 | set +e 18 | apt-get update 19 | set -e 20 | apt-get install -y libxkbcommon-dev:arm64 21 | dpkg -L libxkbcommon-dev:arm64 22 | export PATH=~/usr/lib/aarch64-linux-gnu:$PATH 23 | export RUSTFLAGS='-L /usr/lib/aarch64-linux-gnu' 24 | # hack for maturin wheel repair not picking up rust flags 25 | # https://github.com/PyO3/maturin/discussions/2092#discussioncomment-9648400 26 | cp /usr/lib/aarch64-linux-gnu/libxkbcommon.so.0.0.0 /usr/aarch64-unknown-linux-gnu/aarch64-unknown-linux-gnu/sysroot/lib64/libxkbcommon.so 27 | cp /usr/lib/aarch64-linux-gnu/libxkbcommon.so.0.0.0 /usr/aarch64-unknown-linux-gnu/aarch64-unknown-linux-gnu/sysroot/lib64/libxkbcommon.so.0 28 | elif command -v yum &> /dev/null; then 29 | echo "using yum" 30 | yum install -y libxkbcommon-devel libatomic 31 | 32 | # build pkg-config manually due to a bug in the old version from the repo 33 | cd /tmp 34 | git clone https://github.com/pkgconf/pkgconf 35 | cd pkgconf 36 | ./autogen.sh 37 | ./configure \ 38 | --with-system-libdir=/lib:/usr/lib \ 39 | --with-system-includedir=/usr/include 40 | make 41 | make install 42 | fi 43 | -------------------------------------------------------------------------------- /ci/prepare-ci-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | sudo apt-get install -y libxkbcommon-dev 6 | 7 | python -m venv .env 8 | source .env/bin/activate 9 | pip install maturin -------------------------------------------------------------------------------- /ci/templates/PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: shiro 2 | 3 | pkgname=python-map2 4 | pkgver= 5 | pkgrel=1 6 | pkgdesc="Linux input remapping library" 7 | url="https://github.com/shiro/map2" 8 | arch=('x86_64' 'i686') 9 | license=('MIT') 10 | depends=('python-pip' 'python-wheel' 'python') 11 | depends_x86_64=('libxkbcommon') 12 | source_i686=('lib32-libxkbcommon') 13 | makedepends=() 14 | source_x86_64=("https://github.com/shiro/map2/releases/download/$pkgver/map2-$pkgver-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl") 15 | source_i686=("https://github.com/shiro/map2/releases/download/$pkgver/map2-$pkgver-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl") 16 | sha256sums_x86_64=('') 17 | sha256sums_i686=('') 18 | 19 | 20 | package() { 21 | cd "$srcdir" 22 | PIP_CONFIG_FILE=/dev/null pip install --isolated --root="$pkgdir" --ignore-installed --no-deps *.whl 23 | } 24 | -------------------------------------------------------------------------------- /docs-old/man/map2.1: -------------------------------------------------------------------------------- 1 | .TH MAP2 1 2 | .SH NAME 3 | Map2 \- A scripting language that allows complex key remapping on Linux. 4 | .SH SYNOPSIS 5 | \fBMap2\fR [FLAGS] 6 | .SH FLAGS 7 | .TP 8 | \fB\-v\fR, \fB\-\-verbose\fR 9 | Prints verbose information 10 | 11 | .TP 12 | \fB\-d\fR, \fB\-\-devices\fR 13 | Selects the input devices 14 | .SH DEVICES 15 | In order to capture device input it is necessary to configure which devices should get captured. A list of devices can be specified by providing a device list argument or by defining a default configuration in the user's configuration directory ($XDG_CONFIG_HOME/map2/device.list). 16 | 17 | 18 | .SH LICENSE 19 | MIT 20 | 21 | 22 | .SH EXIT STATUS 23 | .TP 24 | \fB0\fR 25 | Successful program execution. 26 | 27 | .TP 28 | \fB1\fR 29 | Unsuccessful program execution. 30 | 31 | .TP 32 | \fB101\fR 33 | The program panicked. 34 | .SH EXAMPLES 35 | .TP 36 | run a script 37 | \fB$ map2 example.m2\fR 38 | .br 39 | Runs the specified script. 40 | .TP 41 | run a script and capture devices matched by the device list 42 | \fB$ map2 \-d device.list example.m2\fR 43 | .br 44 | Captures devices that match the selectors in `device.list` and runs the script. 45 | 46 | .SH AUTHOR 47 | .P 48 | .RS 2 49 | .nf 50 | shiro 51 | 52 | -------------------------------------------------------------------------------- /docs-old/start-automatically.md: -------------------------------------------------------------------------------- 1 | # Start automatically 2 | 3 | A common use case for map2 scripts is to run all the time, so starting them 4 | automatically in the background on startup / login makes a lot of sense. 5 | There are several methods to do this, most of which are described in detail on 6 | [this Arch Wiki page](https://wiki.archlinux.org/title/Autostarting). 7 | 8 | ## Systemd 9 | 10 | If systemd is installed on the system, it is possible to start scripts on login 11 | by creating a new unit file: 12 | 13 | *~/.config/systemd/user/map2.service:* 14 | 15 | ``` 16 | [Unit] 17 | Description=map2 script 18 | 19 | [Service] 20 | Type=exec 21 | ExecStart=python /path/to/script.py 22 | 23 | [Install] 24 | WantedBy=multi-user.target 25 | ``` 26 | 27 | And running a few simple commands: 28 | 29 | ``` 30 | $ systemctl --user daemon-reload 31 | $ systemctl --user enable map2 32 | $ systemctl --user start map2 33 | ``` 34 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .astro/ 4 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # map2 documentation 2 | 3 | [![Build](https://github.com/shiro/map2/actions/workflows/CI.yml/badge.svg)](https://github.com/shiro/map2/actions/workflows/CI.yml) 4 | 5 | ## Setup 6 | 7 | To install dependencies, run: 8 | 9 | ```bash 10 | yarn 11 | ``` 12 | 13 | ## Development 14 | 15 | To start the development server which will live update the website, run: 16 | 17 | ```bash 18 | yarn dev 19 | ``` 20 | The docs website should now be accessible at `http://localhost:3000/map2`. 21 | 22 | 23 | ## Notes 24 | 25 | This is docs are using the [Astro docs template](https://github.com/advanced-astro/astro-docs-template). 26 | -------------------------------------------------------------------------------- /docs/astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'astro/config'; 2 | import mdx from '@astrojs/mdx'; 3 | import preact from '@astrojs/preact'; 4 | import sitemap from '@astrojs/sitemap'; 5 | import solidJS from "@astrojs/solid-js"; 6 | 7 | 8 | export default defineConfig({ 9 | integrations: [ 10 | mdx(), 11 | sitemap(), 12 | preact({ 13 | compat: true, 14 | include: ["**/*.tsx"], 15 | exclude: ["**/*.solid.tsx"] 16 | }), 17 | solidJS({ 18 | include: ["**/*.solid.tsx"], 19 | }), 20 | ], 21 | markdown: { 22 | shikiConfig: { 23 | experimentalThemes: { 24 | light: "github-light", 25 | dark: "github-dark", 26 | }, 27 | }, 28 | }, 29 | site: "https://shiro.github.io", 30 | base: "/map2", 31 | server: { port: 3000 }, 32 | }); 33 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "scripts": { 6 | "astro": "astro", 7 | "build": "astro check && astro build", 8 | "check": "astro check", 9 | "dev": "astro dev", 10 | "preview": "astro preview", 11 | "start": "astro dev", 12 | "prettier:check": "prettier --check --plugin-search-dir=. .", 13 | "format": "prettier --cache --write --plugin-search-dir=. .", 14 | "lint:scss": "stylelint \"src/**/*.{astro,scss}\"" 15 | }, 16 | "dependencies": { 17 | "@algolia/client-search": "^4.20.0", 18 | "@astrojs/check": "0.3.1", 19 | "@astrojs/preact": "^3.0.1", 20 | "@astrojs/solid-js": "3.0.0", 21 | "@docsearch/css": "^3.5.2", 22 | "@docsearch/react": "^3.5.2", 23 | "@types/node": "^20.9.1", 24 | "astro": "4.0.0-beta.2", 25 | "preact": "^10.19.2", 26 | "solid-js": "1.8.6", 27 | "typescript": "^5.2.2" 28 | }, 29 | "devDependencies": { 30 | "@astrojs/mdx": "^1.1.5", 31 | "@astrojs/sitemap": "^3.0.3", 32 | "@types/html-escaper": "3.0.2", 33 | "astro-robots-txt": "^1.0.0", 34 | "html-escaper": "3.0.3", 35 | "postcss": "^8.4.31", 36 | "postcss-html": "^1.5.0", 37 | "prettier": "^3.1.0", 38 | "sass": "^1.69.5", 39 | "stylelint": "^15.11.0", 40 | "stylelint-config-recommended-scss": "^13.1.0", 41 | "stylelint-config-standard": "^34.0.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /docs/prettier.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require.resolve('prettier-plugin-astro')], 3 | overrides: [ 4 | { 5 | files: '*.astro', 6 | options: { 7 | parser: 'astro' 8 | } 9 | } 10 | ], 11 | singleQuote: true, 12 | semi: false, 13 | trailingComma: 'none' 14 | } 15 | -------------------------------------------------------------------------------- /docs/public/default-og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shiro/map2/b101c60b1220c2538abc808f79c0338d7aacf092/docs/public/default-og-image.png -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shiro/map2/b101c60b1220c2538abc808f79c0338d7aacf092/docs/public/favicon.ico -------------------------------------------------------------------------------- /docs/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /docs/public/fonts/ibm-plex-mono-v15-latin-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shiro/map2/b101c60b1220c2538abc808f79c0338d7aacf092/docs/public/fonts/ibm-plex-mono-v15-latin-italic.woff -------------------------------------------------------------------------------- /docs/public/fonts/ibm-plex-mono-v15-latin-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shiro/map2/b101c60b1220c2538abc808f79c0338d7aacf092/docs/public/fonts/ibm-plex-mono-v15-latin-italic.woff2 -------------------------------------------------------------------------------- /docs/public/fonts/ibm-plex-mono-v15-latin-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shiro/map2/b101c60b1220c2538abc808f79c0338d7aacf092/docs/public/fonts/ibm-plex-mono-v15-latin-regular.woff -------------------------------------------------------------------------------- /docs/public/fonts/ibm-plex-mono-v15-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shiro/map2/b101c60b1220c2538abc808f79c0338d7aacf092/docs/public/fonts/ibm-plex-mono-v15-latin-regular.woff2 -------------------------------------------------------------------------------- /docs/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shiro/map2/b101c60b1220c2538abc808f79c0338d7aacf092/docs/public/logo.png -------------------------------------------------------------------------------- /docs/public/logo.svg: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /docs/public/make-scrollable-code-focusable.js: -------------------------------------------------------------------------------- 1 | Array.from(document.getElementsByTagName('pre')).forEach((element) => { 2 | element.setAttribute('tabindex', '0') 3 | }) 4 | -------------------------------------------------------------------------------- /docs/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:base"] 4 | } 5 | -------------------------------------------------------------------------------- /docs/sandbox.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "infiniteLoopProtection": true, 3 | "hardReloadOnChange": false, 4 | "view": "browser", 5 | "template": "node", 6 | "container": { 7 | "port": 3000, 8 | "startScript": "start", 9 | "node": "16" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /docs/src/components/Footer/Footer.astro: -------------------------------------------------------------------------------- 1 | --- 2 | //import AvatarList from './AvatarList.astro' 3 | //type Props = { 4 | // path: string 5 | //} 6 | //const { path } = Astro.props 7 | --- 8 | 9 |
10 | 11 |
12 | 13 | 20 | -------------------------------------------------------------------------------- /docs/src/components/HeadCommon.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import '../styles/theme.scss' 3 | import '../styles/index.scss' 4 | --- 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 41 | 42 | 43 | 44 | 45 | 46 | 58 | -------------------------------------------------------------------------------- /docs/src/components/HeadSEO.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { CollectionEntry } from 'astro:content' 3 | import { SITE, OPEN_GRAPH } from '../consts' 4 | 5 | type Props = { canonicalUrl: URL } & CollectionEntry<'docs'>['data'] 6 | 7 | const { ogLocale, image, title, description, canonicalUrl } = Astro.props 8 | const formattedContentTitle = `${title} 🚀 ${SITE.title}` 9 | const imageSrc = image?.src ?? OPEN_GRAPH.image.src 10 | const canonicalImageSrc = new URL(imageSrc, Astro.site) 11 | const imageAlt = image?.alt ?? OPEN_GRAPH.image.alt 12 | --- 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 29 | 30 | 31 | 32 | 40 | 41 | 47 | -------------------------------------------------------------------------------- /docs/src/components/Header/AstroLogo.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import SVGLogo from "../../../public/logo.svg?raw"; 3 | --- 4 | 5 | 6 | 7 | 11 | -------------------------------------------------------------------------------- /docs/src/components/Header/Header.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getLanguageFromURL, KNOWN_LANGUAGE_CODES } from '../../languages' 3 | import { SITE } from '../../consts' 4 | import AstroLogo from './AstroLogo.astro' 5 | import SkipToContent from './SkipToContent.astro' 6 | import SidebarToggle from './SidebarToggle' 7 | import LanguageSelect from './LanguageSelect' 8 | //import Search from './Search' 9 | 10 | type Props = { 11 | currentPage: string 12 | } 13 | 14 | const { currentPage } = Astro.props 15 | const lang = getLanguageFromURL(currentPage) 16 | --- 17 | 18 |
19 | 20 | 42 |
43 | 44 | 147 | 148 | 153 | -------------------------------------------------------------------------------- /docs/src/components/Header/LanguageSelect.tsx: -------------------------------------------------------------------------------- 1 | import type { FunctionComponent } from "preact"; 2 | import '../../styles/langSelect.scss' 3 | import { KNOWN_LANGUAGES, langPathRegex } from '../../languages' 4 | 5 | const LanguageSelect: FunctionComponent<{ lang: string }> = ({ lang }) => { 6 | return ( 7 |
8 | 26 | 44 |
45 | ) 46 | } 47 | 48 | export default LanguageSelect 49 | -------------------------------------------------------------------------------- /docs/src/components/Header/Search.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource react */ 2 | import { useState, useCallback, useRef } from 'react' 3 | import { ALGOLIA } from '../../consts' 4 | import '@docsearch/css' 5 | import '../../styles/search.scss' 6 | 7 | import { createPortal } from 'react-dom' 8 | import * as docSearchReact from '@docsearch/react' 9 | 10 | /** FIXME: This is still kinda nasty, but DocSearch is not ESM ready. */ 11 | const DocSearchModal = 12 | docSearchReact.DocSearchModal || 13 | (docSearchReact as any).default.DocSearchModal 14 | const useDocSearchKeyboardEvents = 15 | docSearchReact.useDocSearchKeyboardEvents || 16 | (docSearchReact as any).default.useDocSearchKeyboardEvents 17 | 18 | export default function Search() { 19 | const [isOpen, setIsOpen] = useState(false) 20 | const searchButtonRef = useRef(null) 21 | const [initialQuery, setInitialQuery] = useState('') 22 | 23 | const onOpen = useCallback(() => { 24 | setIsOpen(true) 25 | }, [setIsOpen]) 26 | 27 | const onClose = useCallback(() => { 28 | setIsOpen(false) 29 | }, [setIsOpen]) 30 | 31 | const onInput = useCallback( 32 | (e) => { 33 | setIsOpen(true) 34 | setInitialQuery(e.key) 35 | }, 36 | [setIsOpen, setInitialQuery] 37 | ) 38 | 39 | useDocSearchKeyboardEvents({ 40 | isOpen, 41 | onOpen, 42 | onClose, 43 | onInput, 44 | searchButtonRef 45 | }) 46 | 47 | return ( 48 | <> 49 | 75 | 76 | {isOpen && 77 | createPortal( 78 | { 86 | return items.map((item) => { 87 | // We transform the absolute URL into a relative URL to 88 | // work better on localhost, preview URLS. 89 | const a = document.createElement('a') 90 | a.href = item.url 91 | const hash = a.hash === '#overview' ? '' : a.hash 92 | return { 93 | ...item, 94 | url: `${a.pathname}${hash}` 95 | } 96 | }) 97 | }} 98 | />, 99 | document.body 100 | )} 101 | 102 | ) 103 | } 104 | -------------------------------------------------------------------------------- /docs/src/components/Header/SidebarToggle.tsx: -------------------------------------------------------------------------------- 1 | import type { FunctionalComponent } from 'preact' 2 | import { useState, useEffect } from 'preact/hooks' 3 | 4 | const MenuToggle: FunctionalComponent = () => { 5 | const [sidebarShown, setSidebarShown] = useState(false) 6 | 7 | useEffect(() => { 8 | const body = document.querySelector('body')! 9 | if (sidebarShown) { 10 | body.classList.add('mobile-sidebar-toggle') 11 | } else { 12 | body.classList.remove('mobile-sidebar-toggle') 13 | } 14 | }, [sidebarShown]) 15 | 16 | return ( 17 | 40 | ) 41 | } 42 | 43 | export default MenuToggle 44 | -------------------------------------------------------------------------------- /docs/src/components/Header/SkipToContent.astro: -------------------------------------------------------------------------------- 1 | --- 2 | type Props = {} 3 | --- 4 | 5 | 8 | 9 | 29 | -------------------------------------------------------------------------------- /docs/src/components/LeftSidebar/LeftSidebar.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getLanguageFromURL } from '../../languages' 3 | import { SIDEBAR } from '../../consts' 4 | 5 | type Props = { 6 | currentPage: string 7 | } 8 | 9 | const { currentPage } = Astro.props 10 | const currentPageMatch = currentPage.endsWith('/') 11 | ? currentPage.slice(1, -1) 12 | : currentPage.slice(1) 13 | const langCode = getLanguageFromURL(currentPage) 14 | const sidebar = SIDEBAR[langCode] 15 | --- 16 | 17 | 47 | 48 | 56 | 57 | 116 | -------------------------------------------------------------------------------- /docs/src/components/PageContent/PageContent.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { MarkdownHeading } from 'astro' 3 | import MoreMenu from '../RightSidebar/MoreMenu.astro' 4 | import TableOfContents from '../RightSidebar/TableOfContents' 5 | 6 | type Props = { 7 | title: string 8 | headings: MarkdownHeading[] 9 | editUrl: string 10 | } 11 | 12 | const { title, headings, editUrl } = Astro.props 13 | --- 14 | 15 |
16 |
17 |

{title}

18 | 21 | 22 |
23 | 26 |
27 | 28 | 52 | -------------------------------------------------------------------------------- /docs/src/components/RightSidebar/Example.solid.tsx: -------------------------------------------------------------------------------- 1 | import "solid-js"; 2 | import {createSignal} from "solid-js"; 3 | 4 | const Example = () => { 5 | const [count, setCount] = createSignal(0); 6 | 7 | return ( 8 |
9 | 10 | {count()} 11 | 12 |
13 | ); 14 | }; 15 | 16 | export default Example; 17 | -------------------------------------------------------------------------------- /docs/src/components/RightSidebar/RightSidebar.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { MarkdownHeading } from 'astro' 3 | import TableOfContents from './TableOfContents' 4 | import MoreMenu from './MoreMenu.astro' 5 | 6 | type Props = { 7 | headings: MarkdownHeading[] 8 | editUrl: string 9 | } 10 | 11 | const { headings, editUrl } = Astro.props 12 | --- 13 | 14 | 20 | 21 | 35 | -------------------------------------------------------------------------------- /docs/src/components/RightSidebar/TableOfContents.tsx: -------------------------------------------------------------------------------- 1 | import type { MarkdownHeading } from "astro"; 2 | import type { FunctionalComponent } from "preact"; 3 | import { unescape } from "html-escaper"; 4 | import { useState, useEffect, useRef } from "preact/hooks"; 5 | 6 | type ItemOffsets = { 7 | id: string 8 | topOffset: number 9 | }; 10 | 11 | const TableOfContents: FunctionalComponent<{ headings: MarkdownHeading[] }> = ({ 12 | headings = [] 13 | }) => { 14 | const toc = useRef(); 15 | const onThisPageID = "on-this-page-heading"; 16 | const itemOffsets = useRef([]); 17 | const [currentID, setCurrentID] = useState("overview"); 18 | useEffect(() => { 19 | const getItemOffsets = () => { 20 | const titles = document.querySelectorAll('article :is(h1, h2, h3, h4)') 21 | itemOffsets.current = Array.from(titles).map((title) => ({ 22 | id: title.id, 23 | topOffset: title.getBoundingClientRect().top + window.scrollY 24 | })) 25 | } 26 | 27 | getItemOffsets() 28 | window.addEventListener('resize', getItemOffsets) 29 | 30 | return () => { 31 | window.removeEventListener('resize', getItemOffsets) 32 | } 33 | }, []) 34 | 35 | useEffect(() => { 36 | if (!toc.current) return 37 | 38 | const setCurrent: IntersectionObserverCallback = (entries) => { 39 | for (const entry of entries) { 40 | if (entry.isIntersecting) { 41 | const { id } = entry.target 42 | if (id === onThisPageID) continue 43 | setCurrentID(entry.target.id) 44 | break 45 | } 46 | } 47 | } 48 | 49 | const observerOptions: IntersectionObserverInit = { 50 | // Negative top margin accounts for `scroll-margin`. 51 | // Negative bottom margin means heading needs to be towards top of viewport to trigger intersection. 52 | rootMargin: '-100px 0% -66%', 53 | threshold: 1 54 | } 55 | 56 | const headingsObserver = new IntersectionObserver( 57 | setCurrent, 58 | observerOptions 59 | ) 60 | 61 | // Observe all the headings in the main page content. 62 | document 63 | .querySelectorAll('article :is(h1,h2,h3)') 64 | .forEach((h) => headingsObserver.observe(h)) 65 | 66 | // Stop observing when the component is unmounted. 67 | return () => headingsObserver.disconnect() 68 | }, [toc.current]) 69 | 70 | const onLinkClick = (e) => { 71 | setCurrentID(e.target.getAttribute('href').replace('#', '')) 72 | } 73 | 74 | return ( 75 | <> 76 |

77 | On this page 78 |

79 | 94 | 95 | ) 96 | } 97 | 98 | export default TableOfContents 99 | -------------------------------------------------------------------------------- /docs/src/components/RightSidebar/ThemeToggleButton.scss: -------------------------------------------------------------------------------- 1 | .theme-toggle { 2 | display: inline-flex; 3 | align-items: center; 4 | gap: 0.25em; 5 | padding: 0.33em 0.67em; 6 | border-radius: 99em; 7 | background-color: var(--theme-code-inline-bg); 8 | } 9 | 10 | .theme-toggle > label:focus-within { 11 | outline: 2px solid transparent; 12 | box-shadow: 0 0 0 0.08em var(--theme-accent), 0 0 0 0.12em white; 13 | } 14 | 15 | .theme-toggle > label { 16 | color: var(--theme-code-inline-text); 17 | position: relative; 18 | display: flex; 19 | align-items: center; 20 | justify-content: center; 21 | opacity: 0.5; 22 | } 23 | 24 | .theme-toggle .checked { 25 | color: var(--theme-accent); 26 | opacity: 1; 27 | } 28 | 29 | input[name='theme-toggle'] { 30 | position: absolute; 31 | opacity: 0; 32 | top: 0; 33 | right: 0; 34 | bottom: 0; 35 | left: 0; 36 | z-index: -1; 37 | } 38 | -------------------------------------------------------------------------------- /docs/src/components/RightSidebar/ThemeToggleButton.tsx: -------------------------------------------------------------------------------- 1 | import type { FunctionalComponent } from 'preact' 2 | import { useState, useEffect } from 'preact/hooks' 3 | import './ThemeToggleButton.scss' 4 | 5 | const themes = ['light', 'dark'] 6 | 7 | const icons = [ 8 | 15 | 20 | , 21 | 28 | 29 | 30 | ] 31 | 32 | const ThemeToggle: FunctionalComponent = () => { 33 | const [theme, setTheme] = useState(() => { 34 | if (import.meta.env.SSR) { 35 | return undefined 36 | } 37 | if (typeof localStorage !== undefined && localStorage.getItem('theme')) { 38 | return localStorage.getItem('theme') 39 | } 40 | if (window.matchMedia('(prefers-color-scheme: dark)').matches) { 41 | return "dark"; 42 | } 43 | return "light"; 44 | }) 45 | 46 | useEffect(() => { 47 | const root = document.documentElement 48 | if (theme === 'light') { 49 | root.classList.remove('theme-dark') 50 | } else { 51 | root.classList.add('theme-dark') 52 | } 53 | }, [theme]) 54 | 55 | return ( 56 |
57 | {themes.map((t, i) => { 58 | const icon = icons[i] 59 | const checked = t === theme 60 | return ( 61 | 76 | ) 77 | })} 78 |
79 | ) 80 | } 81 | 82 | export default ThemeToggle 83 | -------------------------------------------------------------------------------- /docs/src/components/ValidKeysTable.solid.tsx: -------------------------------------------------------------------------------- 1 | import {Show, For} from "solid-js/web"; 2 | 3 | import keyEnumCode from "@project/evdev-rs/src/enums.rs?raw"; 4 | import aliasEnumCode from "@project/src/key_defs.rs?raw"; 5 | 6 | 7 | const keys = (() => { 8 | const code = keyEnumCode; 9 | const pat = "pub enum EV_KEY {"; 10 | const fromIdx = code.indexOf(pat) + pat.length; 11 | const toIdx = code.indexOf("}", fromIdx); 12 | 13 | const snippet = code.slice(fromIdx, toIdx); 14 | 15 | const literals = new Set([ 16 | "apostrophe", 17 | "backslash", 18 | "comma", 19 | "dollar", 20 | "dot", 21 | "equal", 22 | "euro", 23 | "grave", 24 | "leftbrace", 25 | "minus", 26 | "rightbrace", 27 | "semicolon", 28 | "slash", 29 | ]); 30 | 31 | return snippet 32 | .split(",") 33 | .map(x => x.trim()) 34 | .map(x => x.slice( 35 | x.startsWith("KEY_") ? "KEY_".length : 0, 36 | x.indexOf(" ")) 37 | ) 38 | .map(x => x.toLowerCase()) 39 | .filter(x => x.length > 1) 40 | .filter(x => !literals.has(x)); 41 | })(); 42 | 43 | const aliases = (() => { 44 | const code = aliasEnumCode; 45 | const pat = "let mut m = HashMap::new();"; 46 | const fromIdx = code.indexOf(pat) + pat.length; 47 | const toIdx = code.indexOf("m\n", fromIdx); 48 | 49 | const snippet = code.slice(fromIdx, toIdx); 50 | 51 | return Object.fromEntries( 52 | snippet 53 | .replaceAll("\n", " ") 54 | .split(";") 55 | .map(x => x.trim()) 56 | .filter(Boolean) 57 | .map(x => new RegExp(`"(.*)".*KEY_([^.]+)`).exec(x)!.slice(1, 3)) 58 | .map(([alias, key]) => [key.toLowerCase(), alias.toLowerCase()]) 59 | ); 60 | })(); 61 | 62 | 63 | const descriptions = { 64 | brl_dot1: "braille dot 1", 65 | brl_dot2: "braille dot 2", 66 | brl_dot3: "braille dot 3", 67 | brl_dot4: "braille dot 4", 68 | brl_dot5: "braille dot 5", 69 | brl_dot6: "braille dot 6", 70 | brl_dot7: "braille dot 7", 71 | brl_dot8: "braille dot 8", 72 | brl_dot9: "braille dot 9", 73 | brl_dot10: "braille dot 10", 74 | btn_left: "left mouse button", 75 | btn_right: "right mouse button", 76 | btn_middle: "middle mouse button", 77 | down: "'down' directional key", 78 | f1: "function 1", 79 | f2: "function 2", 80 | f3: "function 3", 81 | f4: "function 4", 82 | f5: "function 5", 83 | f6: "function 6", 84 | f7: "function 7", 85 | f8: "function 8", 86 | f9: "function 9", 87 | f10: "function 10", 88 | f11: "function 11", 89 | f12: "function 12", 90 | f13: "function 13", 91 | f14: "function 14", 92 | f15: "function 15", 93 | f16: "function 16", 94 | f17: "function 17", 95 | f18: "function 18", 96 | f19: "function 19", 97 | f20: "function 20", 98 | f21: "function 21", 99 | f22: "function 22", 100 | f23: "function 23", 101 | f24: "function 24", 102 | kp0: "keypad 0", 103 | kp1: "keypad 1", 104 | kp2: "keypad 2", 105 | kp3: "keypad 3", 106 | kp4: "keypad 4", 107 | kp5: "keypad 5", 108 | kp6: "keypad 6", 109 | kp7: "keypad 7", 110 | kp8: "keypad 8", 111 | kp9: "keypad 9", 112 | kpasterisk: "keypad '*'", 113 | kpcomma: "keypad ','", 114 | kpdot: "keypad '.'", 115 | kpenter: "keypad 'center'", 116 | kpequal: "keypad '='", 117 | kpjpcomma: "keypad Japanese '、'", 118 | kpleftparen: "keypad '('", 119 | kpminus: "keypad '-'", 120 | kpplus: "keypad '+'", 121 | kpplusminus: "keypad '+/-'", 122 | kprightparen: "keypad ')'", 123 | kpslash: "keypad '/'", 124 | left: "'left' directional key", 125 | leftalt: "left meta", 126 | leftctrl: "left control", 127 | leftmeta: "left meta", 128 | leftshift: "left shift", 129 | numeric_0: "numpad 0", 130 | numeric_1: "numpad 1", 131 | numeric_2: "numpad 2", 132 | numeric_3: "numpad 3", 133 | numeric_4: "numpad 4", 134 | numeric_5: "numpad 5", 135 | numeric_6: "numpad 6", 136 | numeric_7: "numpad 7", 137 | numeric_8: "numpad 8", 138 | numeric_9: "numpad 9", 139 | numeric_a: "numpad 'a'", 140 | numeric_b: "numpad 'b'", 141 | numeric_c: "numpad 'c'", 142 | numeric_d: "numpad 'd'", 143 | numeric_pound: "numpad '£'", 144 | numeric_star: "numpad '*'", 145 | right: "'right' directional key", 146 | rightalt: "right alt", 147 | rightctrl: "right control", 148 | rightmeta: "right meta", 149 | rightshift: "right shift", 150 | up: "'up' directional key", 151 | yen: "JPY (円)", 152 | }; 153 | 154 | 155 | 156 | const ValidKeysTable = () => { 157 | return ( 158 | <> 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | {(key) => 167 | 168 | 175 | 176 | 177 | } 178 | 179 | 180 |
Key namesDescription
169 | 170 | {aliases[key]} 171 |
172 |
173 | {key} 174 |
{descriptions[key]}
181 | 182 | ); 183 | } 184 | 185 | export default ValidKeysTable; 186 | -------------------------------------------------------------------------------- /docs/src/consts.ts: -------------------------------------------------------------------------------- 1 | export const SITE = { 2 | title: 'map2 docs', 3 | description: 'Linux key remapping tool map2 - official documentation', 4 | defaultLanguage: 'en-us' 5 | } as const 6 | 7 | export const OPEN_GRAPH = { 8 | image: { 9 | src: 'logo.png', 10 | alt: 'map2 logo - stylized letters "M" and "2"' 11 | }, 12 | } 13 | 14 | export const KNOWN_LANGUAGES = { 15 | English: 'en' 16 | } as const 17 | export const KNOWN_LANGUAGE_CODES = Object.values(KNOWN_LANGUAGES) 18 | 19 | export const EDIT_URL = `https://github.com/shiro/map2/docs`; 20 | 21 | export const COMMUNITY_INVITE_URL = `https://discord.gg/brKgH43XQN`; 22 | export const DONATE_URL = `https://ko-fi.com/shiroi_usagi`; 23 | 24 | // See "Algolia" section of the README for more information. 25 | export const ALGOLIA = { 26 | indexName: 'XXXXXXXXXX', 27 | appId: 'XXXXXXXXXX', 28 | apiKey: 'XXXXXXXXXX' 29 | } 30 | 31 | export type Sidebar = Record< 32 | (typeof KNOWN_LANGUAGE_CODES)[number], 33 | Record 34 | > 35 | export const SIDEBAR: Sidebar = { 36 | en: { 37 | "Basics": [ 38 | { text: "Introduction", link: "en/basics/introduction" }, 39 | { text: "Install", link: "en/basics/install" }, 40 | { text: "Getting started", link: "en/basics/getting-started" }, 41 | { text: "Keys and key sequences", link: "en/basics/keys-and-key-sequences" }, 42 | { text: "Routing", link: "en/basics/routing" }, 43 | ], 44 | "Advanced": [ 45 | { text: "Secure setup", link: "en/advanced/secure-setup" }, 46 | { text: "Autostart", link: "en/advanced/autostart" }, 47 | ], 48 | "API": [ 49 | { text: "map2", link: "en/api/map2" }, 50 | { text: "Reader", link: "en/api/reader" }, 51 | { text: "Mapper", link: "en/api/mapper" }, 52 | { text: "Text Mapper", link: "en/api/text-mapper" }, 53 | { text: "Chord Mapper", link: "en/api/chord-mapper" }, 54 | { text: "Writer", link: "en/api/writer" }, 55 | { text: "Virtual Writer", link: "en/api/virtual-writer" }, 56 | { text: "Window", link: "en/api/window" }, 57 | ], 58 | "Examples": [ 59 | { text: "Hello world", link: "en/examples/hello-world" }, 60 | { text: "Chords", link: "en/examples/chords" }, 61 | { text: "Text mapping", link: "en/examples/text-mapping" }, 62 | { text: "WASD mouse control", link: "en/examples/wasd-mouse-control" }, 63 | { text: "Keyboard to controller", link: "en/examples/keyboard-to-controller" }, 64 | ] 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /docs/src/content/config.ts: -------------------------------------------------------------------------------- 1 | import { defineCollection, z } from 'astro:content' 2 | import { SITE } from '../consts' 3 | 4 | const docs = defineCollection({ 5 | schema: z.object({ 6 | title: z.string().default(SITE.title), 7 | description: z.string().default(SITE.description), 8 | lang: z.literal('en-us').default(SITE.defaultLanguage), 9 | dir: z.union([z.literal('ltr'), z.literal('rtl')]).default('ltr'), 10 | image: z 11 | .object({ 12 | src: z.string(), 13 | alt: z.string() 14 | }) 15 | .optional(), 16 | ogLocale: z.string().optional() 17 | }) 18 | }) 19 | 20 | export const collections = { docs } 21 | -------------------------------------------------------------------------------- /docs/src/content/docs/en/advanced/autostart.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Autostart' 3 | description: 'Autostart map2 scripts on login' 4 | --- 5 | 6 | A common use case for map2 is to run a script automatically after logging in, the way 7 | to do it depends on your Linux distro and system setup. 8 | 9 | We'll use [Systemd](https://wiki.archlinux.org/title/systemd) since most distros ship with 10 | it, but changing the commands to work on other systems should be pretty easy as well. 11 | 12 | ## Setting things up 13 | 14 | The way to prepare things depends on how you are running your map2 scripts, please check the 15 | [Secure setup](/map2/en/advanced/secure-setup) section. 16 | 17 | If you are running your scripts as **superuser** as shown in the [Getting started](/map2/en/basics/getting-started) 18 | section, please first complete any of the ways in [Secure setup](/map2/en/advanced/secure-setup) and continue 19 | with the appropriate section below. 20 | 21 | 22 | ### The lazy way 23 | 24 | Uset this method if you are using: 25 | 26 | - the **_lazy way_** from the [Secure setup](/map2/en/advanced/secure-setup#the-lazy-way) section. 27 | 28 | 29 | Copy the service definition into `~/.config/systemd/user/map2.service`. 30 | 31 | ``` 32 | [Unit] 33 | Description=map2 autostart service 34 | PartOf=graphical-session.target 35 | 36 | [Service] 37 | ExecStart=python /path/to/my-map2-script.py 38 | Restart=always 39 | RestartSec=5s 40 | 41 | [Install] 42 | WantedBy=graphical-session.target 43 | ``` 44 | 45 | And change `/path/to/my-map2-script.py` to your script path. 46 | 47 | 48 | ### The secure way 49 | 50 | 51 | Uset this method if you are using: 52 | 53 | - the **_secure way_** from the [Secure setup](/map2/en/advanced/secure-setup#the-secure-way) section. 54 | 55 | In the following section, replace `/home/map2/my-map2-script.py` with your script path. 56 | 57 | 58 | Create a shell script file in `/home/map2/autostart.sh`: 59 | 60 | ```bash 61 | #!/bin/bash 62 | # map 2 autostart script 63 | # runs a map2 script by switching to the map2 user 64 | 65 | su map2 -pc 'python /home/map2/my-map2-script.py' 66 | ``` 67 | 68 | And run the following commands: 69 | 70 | ```bash 71 | # make the autostart script executable 72 | chmod +x /home/map2/autostart.sh 73 | 74 | # allow everyone to run the autostart script 75 | echo "ALL ALL=(root) NOPASSWD:SETENV: /home/map2/autostart.sh" | sudo tee -a /etc/sudoers 76 | ``` 77 | 78 | Copy the following into `~/.config/systemd/user/map2.service`: 79 | 80 | ``` 81 | [Unit] 82 | Description=map2 autostart service 83 | PartOf=graphical-session.target 84 | 85 | [Service] 86 | ExecStart=sudo -E /home/map2/autostart.sh 87 | Restart=always 88 | RestartSec=5s 89 | 90 | [Install] 91 | WantedBy=graphical-session.target 92 | ``` 93 | 94 | 95 | ## Running the service 96 | 97 | 98 | Now that we created a service, we need to make sure it works and enable it so it starts automatically. 99 | 100 | ```bash 101 | # tell systemd we edited a service 102 | systemctl --user daemon-reload 103 | 104 | # start the service and make sure it runs the script 105 | systemctl --user start map2 106 | 107 | # after ensuring it works, enable it so it runs on every startup 108 | systemctl --user enable map2 109 | ``` 110 | 111 | Your script should now run automatically when you login to your desktop environment. 112 | -------------------------------------------------------------------------------- /docs/src/content/docs/en/advanced/secure-setup.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Secure setup' 3 | description: 'Setup map2 in a more secure way' 4 | --- 5 | 6 | As discussed in the [Getting started](map2/en/basics/getting-started) section, running your script with 7 | superuser access is not ideal since it allows the script to steal your data, modify your system or 8 | remove system files. This is especially risky when running code that you haven't written yourself. 9 | 10 | 11 | The initial setup **always** requires superuser access, ask your local system administrator for help if necessary. 12 | 13 | 14 | ## The lazy way 15 | 16 | A quick way to avoid running as superuser is to be a member of the `input` group, however this 17 | also allows other processes to listen in on input events (keyloggers, etc.). 18 | If possible, please use [the secure way](#the-secure-way). 19 | Just to demonstrate, we'll go over the *quick and insecure* approach in this section. 20 | 21 | Add the current user into the `input` group. 22 | 23 | ```bash 24 | # allow the current user to intercept input device events 25 | sudo usermod -aG input `whoami` 26 | ``` 27 | 28 | By default, modifying input events always requires superuser access, so we need to change that as 29 | well. 30 | Copy the following into `/etc/udev/rules.d/999-map2.rules`. 31 | 32 | ``` 33 | # Allow the 'input' group to manipulate input events 34 | SUBSYSTEM=="misc", KERNEL=="uinput", MODE="0660", GROUP="input" 35 | ``` 36 | 37 | With this the current user should be able to run map2 scripts without superuser permissions. 38 | 39 | 40 | ## The secure way 41 | 42 | The more secure (and complicated) approach is to create a new user that has exclusive ownership of the 43 | script files and is allowed to intercept events from input devices. 44 | This way, even if a user account gets compromised, it would not be possible to tamper with script files 45 | or spy on input devices. 46 | 47 | 48 | 49 | 50 | Create a new system user called `map2` and set a secure password for it: 51 | 52 | ```bash 53 | # add a new system user called 'map2', also create a home directory 54 | sudo useradd -rm -s /bin/sh map2 55 | # allow it to intercept input device events 56 | sudo usermod -aG input map2 57 | # set a secure password for the new user 58 | sudo passwd map2 59 | ``` 60 | 61 | If you have an existing script, transfer the ownership to the `map2` user and remove all permissions 62 | to the file for other users, so others can't read/modify the script. 63 | We should also move the script to `/home/map2` in order to avoid permission issues. 64 | 65 | ```bash 66 | # transfer all ownership, remove access for other users 67 | sudo chown map2:map2 my-map2-script.py 68 | sudo chmod 700 my-map2-script.py 69 | # move the script to a location owned by the map2 user 70 | sudo mv my-map2-script.py /home/map2 71 | ``` 72 | 73 | To also allow the `input` group to modify input events, 74 | copy the following into 75 | `/etc/udev/rules.d/999-map2.rules`. 76 | 77 | ``` 78 | # Allow the 'input' group to manipulate input events 79 | SUBSYSTEM=="misc", KERNEL=="uinput", MODE="0660", GROUP="input" 80 | ``` 81 | 82 | And apply the configuration changes. 83 | 84 | ```bash 85 | # reload the udev rules since we modified them 86 | sudo udevadm control --reload-rules 87 | sudo udevadm trigger 88 | ``` 89 | 90 | After this, superuser access is no longer needed. 91 | 92 | ### Running the script 93 | 94 | 95 | Now any user can run the script without superuser access, as long as they know the password for the 96 | `map2` user. You can even modify the script that way. 97 | 98 | ```bash 99 | su map2 -pc 'python ~/my-map2-script.py' 100 | ``` 101 | 102 | 103 | ### Optional extra steps 104 | 105 | It's also possible to allow the `map2` user access to only specific input devices rather than all of them. 106 | This is optional and usually not required unless security is very important. 107 | 108 | Change the contents of `/etc/udev/rules.d/999-map2.rules` to: 109 | 110 | ``` 111 | # Allow the 'map2' group to manipulate input events 112 | SUBSYSTEM=="misc", KERNEL=="uinput", MODE="0660", GROUP="map2" 113 | 114 | # Assign specific input devices to the group 'map2' 115 | ATTRS{name}=="Gaming Keyboard", SUBSYSTEM=="input", MODE="0644", GROUP="map2" 116 | ``` 117 | 118 | And modify the filter rules to match the devices you want to grant access to. There are lots of 119 | guides describing udev rules, for example the [Arch Wiki](https://wiki.archlinux.org/title/udev) 120 | explains it pretty well. 121 | 122 | Finally reload the configuration and adjust the permissions. 123 | 124 | ```bash 125 | # reload the udev rules since we modified them 126 | sudo udevadm control --reload-rules 127 | sudo udevadm trigger 128 | 129 | # remove the map2 user from the input group 130 | sudo gpasswd -d map2 input 131 | ``` 132 | -------------------------------------------------------------------------------- /docs/src/content/docs/en/api/chord-mapper.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Chord mapper' 3 | description: 'Chord mapper | map2 API documentation' 4 | --- 5 | 6 | 7 | Creates a mapping layer that triggers on multiple keys being pressed at once or in very quick 8 | succession. 9 | When activated, it outputs a key sequence or calls a user-function. 10 | 11 | ```python 12 | import map2 13 | 14 | mapper = map2.ChordMapper() 15 | 16 | # trigger when "a" & "b" are pressed together 17 | mapper.map(["a", "b"], "c") 18 | 19 | # mapping to sequences is fine too 20 | mapper.map(["z", "x"], "{backspace} wow!") 21 | 22 | # map to user-function 23 | def greet(): print("hello!") 24 | mapper.map(["w", "q"], greet) 25 | ``` 26 | 27 | Supported on: 28 | - ✅ Hyprland 29 | - ✅ X11 30 | - ✅ Gnome (wayland) 31 | - ✅ KDE plasma (wayland) 32 | 33 | 34 | ## Options 35 | 36 | ### model 37 | 38 | ``` 39 | string? 40 | ``` 41 | 42 | Sets the XKB keyboard model. 43 | 44 | ### layout 45 | 46 | ``` 47 | string? 48 | ``` 49 | 50 | Sets the XKB keyboard layout. 51 | 52 | ### variant 53 | 54 | ``` 55 | string? 56 | ``` 57 | 58 | Sets the XKB keyboard variant. 59 | 60 | ### options 61 | 62 | ``` 63 | string? 64 | ``` 65 | 66 | Sets the XKB keyboard options. 67 | 68 | 69 | ## Methods 70 | 71 | ### map(from, to) 72 | 73 | Maps 2 keys, when pressed together, to a different text sequence or user-function. 74 | 75 | - **from**: [key, key] 76 | - **to**: key_sequence | () -> void 77 | -------------------------------------------------------------------------------- /docs/src/content/docs/en/api/map2.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'map2' 3 | description: 'map2 | map2 API documentation' 4 | --- 5 | 6 | 7 | ```python 8 | import map2 9 | 10 | # speicfy the default XKB keyboard layout 11 | map2.default(layout = "us") 12 | 13 | # everything will use the default layout unless specified otherwise 14 | reader = map2.Reader() 15 | mapper = map2.Mapper() 16 | writer = map2.Writer(capabilities = {"keys": True) 17 | 18 | # define the event flow 19 | map2.link([reader, mapper, writer]) 20 | ``` 21 | 22 | 23 | A collection of global functions that interact with other objects. 24 | 25 | 26 | Supported on: 27 | - ✅ Hyprland 28 | - ✅ X11 29 | - ✅ Gnome (wayland) 30 | - ✅ KDE plasma (wayland) 31 | 32 | 33 | ## Options 34 | 35 | This object has no options. 36 | 37 | 38 | 39 | ## Methods 40 | 41 | ### default(**options) 42 | 43 | Sets global default values such as keyboard layout. 44 | 45 | - **options.model**: string? 46 | - **options.layout**: string? 47 | - **options.variant**: string? 48 | - **options.options**: string? 49 | 50 | ### link(path) 51 | 52 | Links objects and defines the event flow. 53 | 54 | - **path**: ([Reader](map2/en/api/reader) | [Mapper](map2/en/api/mapper) | [Writer](map2/en/api/writer))[] 55 | -------------------------------------------------------------------------------- /docs/src/content/docs/en/api/mapper.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Mapper' 3 | description: 'Mapper | map2 API documentation' 4 | --- 5 | 6 | 7 | ```python 8 | import map2 9 | 10 | mapper = map2.Mapper() 11 | 12 | # map key to key 13 | mapper.map("a", "b") 14 | 15 | # map key to key sequence 16 | mapper.map("b", "hello world") 17 | 18 | def user_function1(key, state): 19 | # map "c" to "hello world" 20 | if key == "c": return "hello world" 21 | # forward keys except for "z" 22 | if key != "z": return True 23 | 24 | # map key to user function 25 | mapper.map("c", user_function1) 26 | 27 | # catch all non-mapped keys 28 | mapper.map_fallback("d", user_function1) 29 | 30 | # map key to key 31 | mapper.map_key("d", "tab") 32 | 33 | def user_function2(type, value): 34 | print("move event {}: {}".format(type, value)) 35 | 36 | # map mouse movements, touchscreen taps, etc. 37 | mapper.map_relative(user_function2) 38 | mapper.map_absolute(user_function2) 39 | ``` 40 | 41 | 42 | Creates a mapping layer that can be used to tap into the input event stream, 43 | modify events and call user defined functions. 44 | 45 | Supported on: 46 | - ✅ Hyprland 47 | - ✅ X11 48 | - ✅ Gnome (wayland) 49 | - ✅ KDE plasma (wayland) 50 | 51 | ## Options 52 | 53 | 54 | ### model 55 | 56 | ``` 57 | string? 58 | ``` 59 | 60 | Sets the XKB keyboard model. 61 | 62 | ### layout 63 | 64 | ``` 65 | string? 66 | ``` 67 | 68 | Sets the XKB keyboard layout. 69 | 70 | ### variant 71 | 72 | ``` 73 | string? 74 | ``` 75 | 76 | Sets the XKB keyboard variant. 77 | 78 | ### options 79 | 80 | ``` 81 | string? 82 | ``` 83 | 84 | Sets the XKB keyboard options. 85 | 86 | 87 | 88 | 89 | ## Methods 90 | 91 | ### map(from, to) 92 | 93 | Maps a key to a key sequence. 94 | 95 | - **from**: key 96 | - **to**: key_sequence 97 | 98 | ### map_key(from, to) 99 | 100 | Maps a key to a key. 101 | 102 | - **from**: key 103 | - **to**: key 104 | 105 | ### map_fallback(handler) 106 | 107 | Maps all keys without explicit mappings to a user function 108 | 109 | - **handler**: (key: key, state: "down" | "up" | "repeat") -> string? 110 | 111 | ### map_relative(handler) 112 | 113 | Maps relative movement input events such as mouse moves to a user function. 114 | 115 | - **handler**: (type: string, value: int) -> string? 116 | 117 | ### map_absolute(handler) 118 | 119 | Maps absolute movement input events such as touchscreen taps to a user function. 120 | 121 | - **handler**: (type: string, value: int) -> string? 122 | -------------------------------------------------------------------------------- /docs/src/content/docs/en/api/reader.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Reader' 3 | description: 'Reader | map2 API documentation' 4 | --- 5 | 6 | 7 | ```python 8 | import map2 9 | 10 | # readers intercept all keyboard inputs and forward them 11 | reader = map2.Reader(patterns=["/dev/input/by-id/my-keyboard"]) 12 | 13 | # send keys as if they were read from a physical device 14 | reader.send("{ctrl down}a{ctrl up}") 15 | ``` 16 | 17 | 18 | 19 | All devices matching a pattern will be *grabbed*, meaning all events will be intercepted by map2 20 | and not forwarded to the system. If events are not passed to an output device such as [Writer](/map2/en/api/writer), 21 | they will be lost, please avoid locking your only keyboard when testing. 22 | 23 | 24 | Supported on: 25 | - ✅ Hyprland 26 | - ✅ X11 27 | - ✅ Gnome (wayland) 28 | - ✅ KDE plasma (wayland) 29 | 30 | 31 | ## Options 32 | 33 | 34 | ### patterns 35 | 36 | ``` 37 | string[]? 38 | ``` 39 | 40 | A list of file descriptors to intercept events from. 41 | 42 | The patterns are regular expressions, if you are not familiar with them, consider reading a 43 | [quick tutorial](https://www.regular-expressions.info/quickstart.html). 44 | 45 | Some quick examples: 46 | 47 | - `/dev/input5`: The specific device on `/dev/input5` 48 | - `/dev/input\d+`: All input devices 49 | - `/dev/input/by-id/.*Gaming_Keyboard.*`: All devices who's ID contains `Gaming_Keyboard` 50 | 51 | 52 | 53 | ## Methods 54 | 55 | ### send(input) 56 | 57 | Sends keys as if they were read from a physical device. 58 | 59 | - **input**: key_sequence 60 | -------------------------------------------------------------------------------- /docs/src/content/docs/en/api/text-mapper.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Text mapper' 3 | description: 'Text mapper | map2 API documentation' 4 | --- 5 | 6 | 7 | Creates a text-based mapping layer that triggers on certain key-sequences (hotstrings). 8 | When activated, it erases the trigger sequence by emmiting `backspace` key events and 9 | then emmits the replacement text or calls a user-function. 10 | 11 | ```python 12 | import map2 13 | 14 | # set the output keyboard layout 15 | map2.default(layout = "us") 16 | 17 | mapper = map2.TextMapper() 18 | 19 | # map text to other text 20 | mapper.map("hello", "bye") 21 | 22 | # capitals and special letters are allowed 23 | mapper.map("LaSeRs?", "lAsErS!") 24 | 25 | # map to user-function 26 | def greet(): print("Hello!") 27 | mapper.map("greet", greet) 28 | 29 | # ❌ This won't work, writers can only output keys contained 30 | # in the output keybarod layout. 31 | # Since we specified the 'us' layout above, we can't map to kanji directly. 32 | mapper.map("usagi", "兎") 33 | 34 | # ✅ we can instead use a virtual writer for writing special characters. 35 | # note: not all environments support virtual writers 36 | virtual_writer = map2.VirtualWriter() 37 | def write_special(text): 38 | def fn(): writer_virtual.send(text) 39 | return fn 40 | mapper.map("usagi", write_special("兎")) 41 | ``` 42 | 43 | 44 | 45 | Supported on: 46 | - ✅ Hyprland 47 | - ✅ X11 48 | - ✅ Gnome (wayland) 49 | - ✅ KDE plasma (wayland) 50 | 51 | ## Options 52 | 53 | 54 | ### model 55 | 56 | ``` 57 | string? 58 | ``` 59 | 60 | Sets the XKB keyboard model. 61 | 62 | ### layout 63 | 64 | ``` 65 | string? 66 | ``` 67 | 68 | Sets the XKB keyboard layout. 69 | 70 | ### variant 71 | 72 | ``` 73 | string? 74 | ``` 75 | 76 | Sets the XKB keyboard variant. 77 | 78 | ### options 79 | 80 | ``` 81 | string? 82 | ``` 83 | 84 | Sets the XKB keyboard options. 85 | 86 | 87 | 88 | 89 | ## Methods 90 | 91 | ### map(from, to) 92 | 93 | Maps a text sequence to a different text sequence or user-function. 94 | 95 | - **from**: key_sequence 96 | - **to**: key_sequence | () -> void 97 | -------------------------------------------------------------------------------- /docs/src/content/docs/en/api/virtual-writer.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Virtual Writer' 3 | description: 'VirtualWriter | map2 API documentation' 4 | --- 5 | 6 | 7 | ```python 8 | import map2 9 | 10 | writer = map2.VirtualWriter() 11 | 12 | # send any text 13 | writer.send("hello world") 14 | # including really weird characters 15 | writer.send("∇⋅∇ψ = ρ") 16 | ``` 17 | 18 | Creates a virtual output device that is able to type any text. It's different from a regular 19 | [Writer](/map2/en/api/writer) in that it doesn't simulate a physical device, but rather sends text directly to the 20 | desktop environment. 21 | 22 | Supported on: 23 | - ✅ Hyprland 24 | - ❌ X11 25 | - ❌ Gnome (wayland) 26 | - ❌ KDE plasma (wayland) 27 | 28 | ## Options 29 | 30 | This object has no options. 31 | 32 | 33 | ## Methods 34 | 35 | ### send(input) 36 | 37 | Sends an arbitrary text input to the desktop environment. 38 | 39 | - **input**: string 40 | -------------------------------------------------------------------------------- /docs/src/content/docs/en/api/window.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Window' 3 | description: 'Window | map2 API documentation' 4 | --- 5 | 6 | 7 | ```python 8 | import map2 9 | 10 | window = map2.Window() 11 | 12 | def on_window_change(active_window_class): 13 | if active_window_class == "firefox": 14 | print("firefox is in focus") 15 | else: 16 | print("firefox is not in focus") 17 | 18 | # the user function will be called whenever the active window changes 19 | window.on_window_change(on_window_change) 20 | ``` 21 | 22 | Listens to window change events from the desktop environemnt and calls the provided 23 | user function with the active window information. 24 | 25 | 26 | Supported on: 27 | - ✅ Hyprland 28 | - ✅ X11 29 | - ❌ Gnome (wayland) 30 | - ❌ KDE plasma (wayland) 31 | 32 | 33 | ## Options 34 | 35 | This object has no options. 36 | 37 | 38 | ## Methods 39 | 40 | ### on_window_change(handler) 41 | 42 | Register a user function that gets called when the active window changes. 43 | 44 | - **handler**: (active_window_class: string?) -> None 45 | -------------------------------------------------------------------------------- /docs/src/content/docs/en/api/writer.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Writer' 3 | description: 'Writer | map2 API documentation' 4 | --- 5 | 6 | 7 | ```python 8 | import map2 9 | 10 | # clone existing device 11 | writer1 = map2.Writer(clone_from = "/dev/input/by-id/example-keyboard") 12 | 13 | # create a new virtual device with specific capabilities 14 | writer2 = map2.Writer(capabilities = {"rel": True, "buttons": True}) 15 | ``` 16 | 17 | 18 | Creates a virtual device that can emmit output events and behaves just like 19 | a physical devices. 20 | 21 | 22 | Supported on: 23 | - ✅ Hyprland 24 | - ✅ X11 25 | - ✅ Gnome (wayland) 26 | - ✅ KDE plasma (wayland) 27 | 28 | 29 | ## Options 30 | 31 | 32 | ### clone_from 33 | 34 | ``` 35 | string? 36 | ``` 37 | 38 | Defines which output events the virtual device can emmit based on an existing device. 39 | 40 | 41 | ### capabilities 42 | 43 | ``` 44 | { 45 | "rel": bool?, 46 | "abs": bool?, 47 | "buttons": bool?, 48 | "keys": bool?, 49 | } 50 | ``` 51 | 52 | Defines which output events the virtual device can emmit. 53 | 54 | 55 | ## Methods 56 | 57 | ### send(input) 58 | 59 | Sends input events as if they were received through an input node. 60 | 61 | - **input**: key_sequence 62 | -------------------------------------------------------------------------------- /docs/src/content/docs/en/basics/getting-started.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Getting started' 3 | description: 'Start using map2 for Linux input remapping, a simple guide' 4 | --- 5 | 6 | A map2 script is simply a python file that uses the map2 package. There are many good python tutorials 7 | out there, for example the [W3Schools python tutorial](https://www.w3schools.com/python). 8 | 9 | ## Running a script 10 | 11 | In most Linux setups, a regular user lacks the permissions to intercept input events for security reasons. 12 | If you have superuser permissions, you can simply run your script as the superuser 13 | (the `-E` flag is important to run in the current environment). 14 | 15 | ```bash 16 | sudo -E python my-map2-script.py 17 | ``` 18 | 19 | This is somewhat risky as the script has access to copy your data, modify files and even remove system files. 20 | Use this method only for code you trust or have written yourself! 21 | 22 | For a more secure setup see the [Secure setup](/map2/en/advanced/secure-setup) section. 23 | 24 | ## Input devices 25 | 26 | On Linux, all connected input devices are listed in `/dev/inputX` where `X` is a number. 27 | To get more information about a device (label, ID, etc.), the following command can be used: 28 | 29 | ```bash 30 | udevadm info -q all -a /dev/inputX 31 | ``` 32 | 33 | Some devices will also show up in `/dev/input/by-id` and `/dev/input/by-path`. This devices 34 | are just symbolic links to the appropriate `/dev/inputX` device, but with more 35 | descriptive names. 36 | 37 | 38 | 39 | ## My first map2 script 40 | 41 | Now that we know which input device we want to map on, let's write a short python script! 42 | 43 | ```python 44 | import time 45 | import map2 46 | 47 | # readers intercept all keyboard inputs and forward them 48 | reader = map2.Reader(patterns=["/dev/input/by-id/my-keyboard"]) 49 | 50 | # mappers change inputs, you can also chain multiple mappers! 51 | mapper = map2.Mapper() 52 | 53 | # writers create new virtual devices we can write into 54 | writer = map2.Writer(clone_from = "/dev/input/by-id/my-keyboard") 55 | 56 | # finally, link nodes to control the event flow 57 | map2.link([reader, mapper, writer]) 58 | 59 | mapper.map("a", "hello world") 60 | 61 | # keep running for 7 seconds 62 | time.sleep(7) 63 | ``` 64 | 65 | After running the script, pressing the `a` key should emit `hello world` instead! 66 | -------------------------------------------------------------------------------- /docs/src/content/docs/en/basics/install.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Install' 3 | description: 'Install map2 using one of various distribution options' 4 | --- 5 | 6 | 7 | ## Using pip 8 | 9 | It's safe to install the package globally, alternatively it also works in virtual envs. 10 | 11 | ```bash 12 | pip install map2 13 | ``` 14 | 15 | ## AUR (Arch user Repository) 16 | 17 | Useful for people running Arch Linux, Manjaro, etc. 18 | 19 | ```bash 20 | pacman -S python-map2 21 | ``` 22 | 23 | ## Building from source 24 | 25 | If you want to build the source code yourself, make sure you have `rust` and `cargo` installed 26 | and clone the repository. 27 | 28 | ```bash 29 | # clone the repository 30 | git clone https://github.com/shiro/map2.git 31 | cd map2 32 | 33 | # setup the environemnt 34 | python -m venv .env 35 | source .env/bin/activate 36 | pip install maturin patchelf 37 | 38 | # run build 39 | maturin develop 40 | ``` 41 | 42 | ## Next steps 43 | 44 | To get started, check out the [Getting started](/map2/en/basics/getting-started) page for a 45 | basic guide on writing and running map2 scripts. 46 | -------------------------------------------------------------------------------- /docs/src/content/docs/en/basics/introduction.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Introduction' 3 | description: 'map2 documentation introduction' 4 | --- 5 | 6 | **Welcome to map2** 7 | 8 | Want to remap your input devices like keyboards, mice, controllers and more? 9 | There's nothing you can't remap with **map2**! 10 | 11 | - 🖱️ **Remap keys, mouse events, controllers, pedals, and more!** 12 | - 🔧 **Highly configurable**, using Python 13 | - 🚀 **Blazingly fast**, written in Rust 14 | - 📦 **Tiny install size** (around 5Mb), almost no dependencies 15 | - ❤️ **Open source**, made with love 16 | 17 | Let's look at an example: 18 | 19 | 20 | ```python 21 | import map2 22 | 23 | # readers intercept all keyboard inputs and forward them 24 | reader = map2.Reader(patterns=["/dev/input/by-id/my-keyboard"]) 25 | # mappers change inputs, you can also chain multiple mappers! 26 | mapper = map2.Mapper() 27 | # writers create new virtual devices we can write into 28 | writer = map2.Writer(clone_from = "/dev/input/by-id/my-keyboard") 29 | # finally, link nodes to control the event flow 30 | map2.link([reader, mapper, writer]) 31 | 32 | # map the "a" key to "B" 33 | mapper.map("a", "B") 34 | 35 | # map "CTRL + ALT + u" to "META + SHIFT + w" 36 | mapper.map("^!u", "#+w") 37 | 38 | # key sequences are also supported 39 | mapper.map("s", "hello world!") 40 | 41 | # use the full power of Python using functions 42 | def custom_function(key, state): 43 | print("called custom function") 44 | 45 | # custom conditions and complex sequences 46 | if key == "d": 47 | return "{ctrl down}a{ctrl up}" 48 | return True 49 | 50 | mapper.map("d", custom_function) 51 | ``` 52 | 53 | For the next step, check the [Install](/map2/en/basics/install) page and the 54 | [Getting started](/map2/en/basics/getting-started) page. 55 | -------------------------------------------------------------------------------- /docs/src/content/docs/en/basics/keys-and-key-sequences.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Keys and key sequences' 3 | description: 'Learn about map2 keys and key sequences' 4 | --- 5 | import ValidKeysTable from "@root/components/ValidKeysTable.solid"; 6 | 7 | 8 | All of map2's functions that deal with mapping keys and emmiting virtual input events accept 9 | a specific syntax for defining such events. 10 | 11 | ## Single keys 12 | 13 | Let's look at an example, the [Mapper::map_key(key, key)](/map2/en/api/mapper) function: 14 | 15 | ```python 16 | import map2 17 | 18 | mapper = map2.Mapper() 19 | mapper.map_key("a", "tab") 20 | ``` 21 | 22 | This function maps the "a" key to the "tab" key and expects a `key` type for both sides. 23 | When functions expect a `key`, it means that only a single key with optional modifiers is allowed. 24 | 25 | Passing in additional modifiers is possible by prepending the keys with one or more of the following special 26 | characters: 27 | 28 | - `^`: ctrl 29 | - `!`: alt 30 | - `+`: shift 31 | - `#`: meta 32 | 33 | Let's map `ALT + a` to `CTRL + tab`: 34 | 35 | ```python 36 | import map2 37 | 38 | mapper = map2.Mapper() 39 | 40 | # "ALT + b" to "CTRL + tab" 41 | mapper.map_key("!b", "^tab") 42 | 43 | # if we want to map the "!" key, we need to escape it with "\" 44 | mapper.map_key("\\!", "^tab") 45 | 46 | # note that we used two "\" since it's a python string 47 | ``` 48 | 49 | *Note*: Keys are case-sensitive except special keys discussed in the next section. 50 | 51 | 52 | ## Key sequences 53 | 54 | Sometimes functions accept more than one key, in which case we need to use the more explicit syntax. 55 | Let's look at the [Mapper::map(key, key_sequence)](/map2/en/api/mapper) function: 56 | 57 | ```python 58 | mapper.map("!a", "Hello!") 59 | mapper.map("b", "{ctrl down}{tab}{ctrl up}") 60 | 61 | # mixing regular characters with special ones is also allowed 62 | mapper.map("#c", "type this and CTRL+w{ctrl down}w{ctrl up}") 63 | ``` 64 | 65 | Notice that the first argument is a `key` type while the second argument is a `key_sequence`. 66 | The special modifier characters are treated as normal characters, instead there are only two 67 | special characters in sequences: `{` and `}`. 68 | 69 | Special keys now need to be surrounded by curly braces, i.e. "tab" becomes `{tab}`, which 70 | will result in tab being pressed and released right after. 71 | 72 | In many cases, we want a key to be held for some time, which can be achieved by specifying a 73 | `state` after the key name, i.e. `{ctrl down}` will press the control key, but not release it. 74 | 75 | Valid states are: 76 | - `down` 77 | - `up` 78 | - `repeat` 79 | 80 | 81 | ## Special key list 82 | 83 | Here's a list of all special key names you can use with `{KEY_NAME}`. 84 | 85 | 86 | -------------------------------------------------------------------------------- /docs/src/content/docs/en/basics/routing.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Routing' 3 | description: 'Routing using map2: define the input event flow' 4 | --- 5 | 6 | Routing in map2 refers to linking nodes such as [Reader](/map2/en/api/reader) and [Writer](/map2/en/api/writer), 7 | defining the input event flow chain. 8 | 9 | Let's look at a basic example: 10 | 11 | 12 | ```python 13 | import map2 14 | 15 | reader_kbd = map2.Reader(patterns=["/dev/input/by-id/example-keyboard"]) 16 | reader_mouse = map2.Reader(patterns=["/dev/input/by-id/example-mouse"]) 17 | 18 | mapper_kbd = map2.Mapper() 19 | mapper_mouse = map2.Mapper() 20 | 21 | writer_kbd = map2.Writer(clone_from = "/dev/input/by-id/example-keyboard") 22 | writer_mouse = map2.Writer(clone_from = "/dev/input/by-id/example-mouse") 23 | 24 | map2.link([reader_kbd, mapper_kbd, writer_kbd]) 25 | map2.link([reader_mouse, mapper_mouse, writer_mouse]) 26 | ``` 27 | 28 | Here, we define two separate event chains, one for each input device, routing events 29 | from the respective reader, through a mapper and to a writer. 30 | 31 | ## Nodes 32 | 33 | Each object that can be placed in a chain is called a node. 34 | 35 | There exist 3 types of nodes: 36 | 37 | - **input**: needs to be at the beginning of a chain 38 | - **passthrough**: can't be at the beginning or end of a chain 39 | - **output**: needs to be at the end of a chain 40 | 41 | A good example for the 3 types of nodes are [Reader](/map2/en/api/reader), 42 | [Mapper](/map2/en/api/mapper) and [Writer](/map2/en/api/writer) respectively. 43 | 44 | 45 | ### Input nodes 46 | 47 | Input nodes collect input events, either from a physical device or from 48 | other inputs, and pass them on to the next node in the chain. 49 | 50 | Currently every input node can only appear in a **single chain**. 51 | This means the following code is invalid: 52 | 53 | ```python 54 | import map2 55 | 56 | reader = map2.Reader(patterns=["/dev/input/by-id/example-keyboard"]) 57 | writer1 = map2.Writer(clone_from = "/dev/input/by-id/example-keyboard1") 58 | writer2 = map2.Writer(clone_from = "/dev/input/by-id/example-keyboard1") 59 | 60 | # error: every reader can only appear in a single chain 61 | map2.link([reader, writer1]) 62 | map2.link([reader, writer2]) 63 | ``` 64 | 65 | ### Passthrough nodes 66 | 67 | Passthrough nodes receive input events from the previous node in the chain, 68 | and pass them on to the next node in the chain, potentially modifying, 69 | removing or creating new input events. 70 | 71 | A passtrhough node can appear in more than one chain at a time, let's look at 72 | an example: 73 | 74 | ```python 75 | import map2 76 | 77 | reader1 = map2.Reader(patterns=["/dev/input/by-id/example-keyboard-1"]) 78 | reader2 = map2.Reader(patterns=["/dev/input/by-id/example-keyboard-1"]) 79 | mapper = map2.Mapper() 80 | writer1 = map2.Writer(clone_from = "/dev/input/by-id/example-keyboard-1") 81 | writer2 = map2.Writer(clone_from = "/dev/input/by-id/example-keyboard-1") 82 | 83 | map2.link([reader1, mapper, writer1]) 84 | map2.link([reader2, mapper, writer2]) 85 | ``` 86 | 87 | In this example, events from `reader1` flow through `mapper` and into `writer1`, while 88 | events from `reader2` flow through `mapper` into `writer2`. 89 | 90 | An important thing to note is, that the modifier state for each chain is separate, i.e. 91 | emitting `shift down` from `reader1` does not affect the mapping behaviour of 92 | inputs coming from `reader2`. 93 | 94 | It's also possible to chain multiple passthrough nodes. 95 | 96 | ```python 97 | import map2 98 | 99 | reader = map2.Reader(patterns=["/dev/input/by-id/example-keyboard-1"]) 100 | mapper1 = map2.Mapper() 101 | mapper2 = map2.Mapper() 102 | mapper3 = map2.Mapper() 103 | writer = map2.Writer(clone_from = "/dev/input/by-id/example-keyboard-1") 104 | 105 | map2.link([reader, mapper1, mapper2, mapper3, writer]) 106 | ``` 107 | 108 | This can be useful for creating *mapping layers*, where each layer maps independently 109 | on the inputs received from the previous layer. 110 | 111 | ### Output nodes 112 | 113 | Output nodes consume events and usually pass them to a physical device, to the desktop 114 | environment, etc. 115 | 116 | Linking multiple chains to an output node is allowed, let's look at an example: 117 | 118 | ```python 119 | import map2 120 | 121 | reader1 = map2.Reader(patterns=["/dev/input/by-id/example-keyboard-1"]) 122 | reader2 = map2.Reader(patterns=["/dev/input/by-id/example-keyboard-1"]) 123 | writer = map2.Writer(clone_from = "/dev/input/by-id/example-keyboard-1") 124 | 125 | map2.link([reader1, writer]) 126 | map2.link([reader2, writer]) 127 | ``` 128 | 129 | In this example, a single writer consumes events from multiple chains. 130 | -------------------------------------------------------------------------------- /docs/src/content/docs/en/examples/chords.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Chords' 3 | description: 'TODO' 4 | --- 5 | 6 | import { Code } from 'astro:components'; 7 | import code from "@examples/chords.py?raw"; 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /docs/src/content/docs/en/examples/hello-world.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Hello world' 3 | description: 'Creating new virtual input events' 4 | --- 5 | 6 | import { Code } from 'astro:components'; 7 | import code from "@examples/hello_world.py?raw"; 8 | 9 | 10 | 11 | 12 | To emmit custom events, we need an output device, so we create a virtual one using [Writer](/map2/en/api/writer). 13 | The device needs the `keys` capability in order to emmit keyboard key events. 14 | 15 | Since we don't want to intercept a physical input device, but instead send events 16 | programatically, we create a [Reader](/map2/en/api/reader). 17 | 18 | Finally we need to link the input and output devices, so events can flow. 19 | -------------------------------------------------------------------------------- /docs/src/content/docs/en/examples/keyboard-to-controller.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Keyboard to controller' 3 | description: 'Control a virtual controller using your keyboard' 4 | --- 5 | 6 | import { Code } from 'astro:components'; 7 | import code from "@examples/keyboard_to_controller.py?raw"; 8 | 9 | 10 | 11 | 12 | 13 | Creates a new virtual controller device and binds keyboard buttons to 14 | various actions. 15 | 16 | This example simulates a controller with 2 joysticks, a dpad, A/B/X/Y buttons, 17 | start/select buttons and 2 shoulder buttons on each side. 18 | -------------------------------------------------------------------------------- /docs/src/content/docs/en/examples/text-mapping.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Text mapping' 3 | description: 'TODO' 4 | --- 5 | 6 | import { Code } from 'astro:components'; 7 | import code from "@examples/text_mapping.py?raw"; 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /docs/src/content/docs/en/examples/wasd-mouse-control.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'WASD mouse control' 3 | description: 'Control the mouse with WASD directional keys' 4 | --- 5 | 6 | import { Code } from 'astro:components'; 7 | import code from "@examples/wasd_mouse_control.py?raw"; 8 | 9 | 10 | 11 | 12 | 13 | Allows moving the mouse by binding the WASD directional keys to functions 14 | that emmit mouse move events. 15 | 16 | This script uses a custom interval implementation to control how much the mouse 17 | should be moved. An alternative approach would be binding the WASD `repeat` state 18 | to move the mouse every time a `repeat` key event is received. 19 | -------------------------------------------------------------------------------- /docs/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | interface ImportMetaEnv { 5 | readonly GITHUB_TOKEN: string | undefined 6 | } 7 | 8 | interface ImportMeta { 9 | readonly env: ImportMetaEnv 10 | } 11 | -------------------------------------------------------------------------------- /docs/src/languages.ts: -------------------------------------------------------------------------------- 1 | import { KNOWN_LANGUAGES, KNOWN_LANGUAGE_CODES } from './consts' 2 | export { KNOWN_LANGUAGES, KNOWN_LANGUAGE_CODES } 3 | 4 | export const langPathRegex = /\/([a-z]{2}-?[A-Z]{0,2})\// 5 | 6 | export function getLanguageFromURL(pathname: string) { 7 | const langCodeMatch = pathname.match(langPathRegex) 8 | const langCode = langCodeMatch ? langCodeMatch[1] : 'en' 9 | return langCode as (typeof KNOWN_LANGUAGE_CODES)[number] 10 | } 11 | -------------------------------------------------------------------------------- /docs/src/layouts/MainLayout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { MarkdownHeading } from 'astro' 3 | import type { CollectionEntry } from 'astro:content' 4 | import HeadCommon from '../components/HeadCommon.astro' 5 | import HeadSEO from '../components/HeadSEO.astro' 6 | import Header from '../components/Header/Header.astro' 7 | import PageContent from '../components/PageContent/PageContent.astro' 8 | import LeftSidebar from '../components/LeftSidebar/LeftSidebar.astro' 9 | import RightSidebar from '../components/RightSidebar/RightSidebar.astro' 10 | //import Footer from '../components/Footer/Footer.astro' 11 | import { EDIT_URL, SITE } from '../consts' 12 | 13 | type Props = CollectionEntry<'docs'>['data'] & { 14 | headings: MarkdownHeading[] 15 | } 16 | 17 | const { headings, ...data } = Astro.props 18 | const canonicalURL = new URL(Astro.url.pathname, Astro.site) 19 | const currentPage = Astro.url.pathname 20 | .replace(/\/$/, '') 21 | .replace(/\/map2\//, '\/'); 22 | const currentFile = `src/content/docs${currentPage}.mdx` 23 | const editUrl = `${EDIT_URL}/${currentFile}` 24 | --- 25 | 26 | 27 | 28 | 29 | 30 | 31 | {`${data.title} | ${SITE.title}`} 32 | 33 | 106 | 121 | 122 | 123 | 124 |
125 |
126 | 129 |
130 | 131 | 132 | 133 |
134 | 137 |
138 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /docs/src/pages/[...slug].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { type CollectionEntry, getCollection } from 'astro:content' 3 | import MainLayout from '../layouts/MainLayout.astro' 4 | 5 | export async function getStaticPaths() { 6 | const docs = await getCollection('docs') 7 | return docs.map((entry) => ({ 8 | params: { 9 | slug: entry.slug 10 | }, 11 | props: entry 12 | })) 13 | } 14 | type Props = CollectionEntry<'docs'> 15 | 16 | const post = Astro.props 17 | const { Content, headings } = await post.render() 18 | --- 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /docs/src/pages/index.astro: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /docs/src/styles/langSelect.scss: -------------------------------------------------------------------------------- 1 | $color_1: var(--theme-text-light); 2 | $color_2: var(--theme-text); 3 | $font-family_1: inherit; 4 | $background-color_1: var(--theme-bg); 5 | $border-color_1: var(--theme-text-lighter); 6 | $border-color_2: var(--theme-text-light); 7 | 8 | .language-select { 9 | flex-grow: 1; 10 | width: 48px; 11 | box-sizing: border-box; 12 | margin: 0; 13 | padding: 0.33em 0.5em; 14 | overflow: visible; 15 | font-weight: 500; 16 | font-size: 1rem; 17 | font-family: $font-family_1; 18 | line-height: inherit; 19 | background-color: $background-color_1; 20 | border-color: $border-color_1; 21 | color: $color_1; 22 | border-style: solid; 23 | border-width: 1px; 24 | border-radius: 0.25rem; 25 | outline: 0; 26 | cursor: pointer; 27 | transition-timing-function: ease-out; 28 | transition-duration: 0.2s; 29 | transition-property: border-color, color; 30 | -webkit-font-smoothing: antialiased; 31 | padding-left: 30px; 32 | padding-right: 1rem; 33 | } 34 | .language-select-wrapper { 35 | .language-select { 36 | &:hover { 37 | color: $color_2; 38 | border-color: $border-color_2; 39 | } 40 | &:focus { 41 | color: $color_2; 42 | border-color: $border-color_2; 43 | } 44 | } 45 | color: $color_1; 46 | position: relative; 47 | > svg { 48 | position: absolute; 49 | top: 7px; 50 | left: 10px; 51 | pointer-events: none; 52 | } 53 | } 54 | @media (min-width: 50em) { 55 | .language-select { 56 | width: 100%; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /docs/src/styles/search.scss: -------------------------------------------------------------------------------- 1 | $color_1: var(--theme-text-light); 2 | $color_2: var(--theme-text); 3 | $font-family_1: inherit; 4 | $font-family_2: var(--font-mono); 5 | $background-color_1: var(--theme-divider); 6 | $border-color_1: var(--theme-divider); 7 | $border-color_2: var(--theme-text-light); 8 | $border-color_3: var(--theme-text-lighter); 9 | 10 | :root { 11 | --docsearch-primary-color: var(--theme-accent); 12 | --docsearch-logo-color: var(--theme-text); 13 | } 14 | .search-input { 15 | flex-grow: 1; 16 | box-sizing: border-box; 17 | width: 100%; 18 | margin: 0; 19 | padding: 0.33em 0.5em; 20 | overflow: visible; 21 | font-weight: 500; 22 | font-size: 1rem; 23 | font-family: $font-family_1; 24 | line-height: inherit; 25 | background-color: $background-color_1; 26 | border-color: $border-color_1; 27 | color: $color_1; 28 | border-style: solid; 29 | border-width: 1px; 30 | border-radius: 0.25rem; 31 | outline: 0; 32 | cursor: pointer; 33 | transition-timing-function: ease-out; 34 | transition-duration: 0.2s; 35 | transition-property: border-color, color; 36 | -webkit-font-smoothing: antialiased; 37 | &:hover { 38 | color: $color_2; 39 | border-color: $border-color_2; 40 | &::placeholder { 41 | color: $color_1; 42 | } 43 | } 44 | &:focus { 45 | color: $color_2; 46 | border-color: $border-color_2; 47 | &::placeholder { 48 | color: $color_1; 49 | } 50 | } 51 | &::placeholder { 52 | color: $color_1; 53 | } 54 | } 55 | .search-hint { 56 | position: absolute; 57 | top: 7px; 58 | right: 19px; 59 | padding: 3px 5px; 60 | display: none; 61 | align-items: center; 62 | justify-content: center; 63 | letter-spacing: 0.125em; 64 | font-size: 13px; 65 | font-family: $font-family_2; 66 | pointer-events: none; 67 | border-color: $border-color_3; 68 | color: $color_1; 69 | border-style: solid; 70 | border-width: 1px; 71 | border-radius: 0.25rem; 72 | line-height: 14px; 73 | } 74 | .DocSearch-Modal { 75 | .DocSearch-Hit { 76 | a { 77 | box-shadow: none; 78 | border: 1px solid var(--theme-accent); 79 | } 80 | } 81 | } 82 | @media (min-width: 50em) { 83 | .search-hint { 84 | display: flex; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict", 3 | "compilerOptions": { 4 | "jsx": "preserve", 5 | "skipLibCheck": true, 6 | "strictNullChecks": true, 7 | "baseUrl": ".", 8 | "paths": { 9 | "@project/*": [ "../*" ], 10 | "@root/*": [ "src/*" ], 11 | "@examples/*": [ "../examples/*" ], 12 | "@components/*": [ "src/components/*" ], 13 | "react": ["./node_modules/preact/compat/"], 14 | "react-dom": ["./node_modules/preact/compat/"] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /evdev-rs/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | *.swp 4 | *.swo 5 | -------------------------------------------------------------------------------- /evdev-rs/.travis.yml: -------------------------------------------------------------------------------- 1 | os: linux 2 | dist: xenial 3 | language: rust 4 | rust: 5 | - stable 6 | - beta 7 | - nightly 8 | 9 | env: 10 | global: 11 | - TARGET=x86_64-unknown-linux-gnu 12 | - PKG_CONFIG_PATH=$HOME/local/lib/pkgconfig 13 | - LD_LIBRARY_PATH=$HOME/local/lib:$LD_LIBRARY_PATH 14 | 15 | jobs: 16 | include: 17 | - env: TARGET=arm-unknown-linux-gnueabi 18 | addons: 19 | apt: 20 | packages: 21 | - gcc-arm-linux-gnueabi 22 | - libc6-armel-cross 23 | - libc6-dev-armel-cross 24 | - env: TARGET=arm-unknown-linux-gnueabihf 25 | addons: 26 | apt: 27 | packages: 28 | - gcc-arm-linux-gnueabihf 29 | - libc6-armhf-cross 30 | - libc6-dev-armhf-cross 31 | allow_failures: 32 | - rust: nightly 33 | 34 | addons: 35 | apt: 36 | packages: 37 | - build-essential 38 | 39 | before_script: 40 | - pip install 'travis-cargo<0.2' --user && export PATH=$HOME/.local/bin:$PATH 41 | - rustup target add $TARGET 42 | - rustup component add rustfmt 43 | 44 | script: 45 | - cargo fmt -- --check 46 | - travis_retry cargo build --target $TARGET --verbose 47 | - travis_retry cargo build --target $TARGET --all-features --verbose 48 | - | 49 | if [ $TARGET == "x86_64-unknown-linux-gnu" ] 50 | then 51 | sudo --preserve-env env "PATH=$PATH" cargo test --verbose 52 | sudo --preserve-env env "PATH=$PATH" cargo test --all-features --verbose 53 | fi 54 | - cargo doc --no-deps --all-features -p evdev-sys -p evdev-rs 55 | 56 | after_success: 57 | - travis-cargo --only stable doc-upload 58 | - travis-cargo coveralls 59 | 60 | notifications: 61 | email: 62 | on_success: never 63 | -------------------------------------------------------------------------------- /evdev-rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "evdev-rs" 3 | version = "0.5.0" 4 | authors = ["Nayan Deshmukh "] 5 | license = "MIT/Apache-2.0" 6 | keywords = ["evdev"] 7 | readme = "README.md" 8 | repository = "https://github.com/ndesh26/evdev-rs" 9 | homepage = "https://github.com/ndesh26/evdev-rs" 10 | documentation = "http://ndesh26.github.io/evdev-rs" 11 | edition = "2018" 12 | description = """ 13 | Bindings to libevdev for interacting with evdev devices. It moves the 14 | common tasks when dealing with evdev devices into a library and provides 15 | a library interface to the callers, thus avoiding erroneous ioctls, etc. 16 | """ 17 | 18 | [features] 19 | default = [] 20 | 21 | [dependencies] 22 | serde = { version = "1.0", default-features = false, features=["derive"], optional = true } 23 | evdev-sys = { path = "evdev-sys", version = "0.2.2" } 24 | libc = "0.2.67" 25 | bitflags = "1.2.1" 26 | log = "0.4.8" 27 | 28 | [package.metadata.docs.rs] 29 | features = ["serde"] -------------------------------------------------------------------------------- /evdev-rs/README.md: -------------------------------------------------------------------------------- 1 | # evdev-rs 2 | 3 | [![Build Status](https://travis-ci.org/ndesh26/evdev-rs.svg?branch=master)](https://travis-ci.org/ndesh26/evdev-rs) 4 | [![Latest Version](https://img.shields.io/crates/v/evdev-rs.svg)](https://crates.io/crates/evdev-rs) 5 | [![Documentation](https://docs.rs/evdev-rs/badge.svg)](https://docs.rs/evdev-rs) 6 | 7 | A Rust wrapper for libevdev 8 | 9 | ```toml 10 | # Cargo.toml 11 | [dependencies] 12 | evdev-rs = "0.5.0" 13 | ``` 14 | 15 | to enable serialization support, enable the feature "serde" 16 | ```toml 17 | # Cargo.toml 18 | [dependencies] 19 | evdev-rs = { version = "0.5.0", features = ["serde"] } 20 | ``` 21 | 22 | Why a libevdev wrapper? 23 | ----------------------- 24 | The evdev protocol is simple, but quirky, with a couple of behaviors that 25 | are non-obvious. libevdev transparently handles some of those quirks. 26 | 27 | The evdev crate on [1] is an implementation of evdev in Rust. Nothing wrong 28 | with that, but it will miss out on any more complex handling that libevdev 29 | provides. 30 | 31 | [1] https://github.com/cmr/evdev/blob/master/src/lib.rs 32 | 33 | Development 34 | ----------- 35 | 36 | `src/enums.rs` can be generated by running `./tools/make-enums.sh`. 37 | -------------------------------------------------------------------------------- /evdev-rs/TODO.md: -------------------------------------------------------------------------------- 1 | ## These function need to implemented in evdev-rs 2 | 3 | * `int libevdev_kernel_set_led_values(struct libevdev *dev, ...);` 4 | 5 | ## We need to define this functions types and the corresponding functions 6 | 7 | * libevdev_log_func_t 8 | * `void libevdev_set_log_function(libevdev_log_func_t logfunc, void *data);` 9 | * libevdev_device_log_func_t 10 | * `void libevdev_set_device_log_function(struct libevdev *dev, 11 | libevdev_device_log_func_t logfunc, 12 | enum libevdev_log_priority priority, 13 | void *data);` 14 | 15 | ## Add Documentation 16 | -------------------------------------------------------------------------------- /evdev-rs/evdev-sys/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "evdev-sys" 3 | version = "0.2.5" 4 | authors = ["Nayan Deshmukh Option<(u32, u32, u32)> { 10 | let mut major_minor_patch = ver_str 11 | .split(".") 12 | .map(|str| str.parse::().unwrap()); 13 | let major = major_minor_patch.next()?; 14 | let minor = major_minor_patch.next()?; 15 | let patch = major_minor_patch.next()?; 16 | Some((major, minor, patch)) 17 | } 18 | 19 | fn main() -> Result<(), Box> { 20 | if env::var_os("TARGET") == env::var_os("HOST") { 21 | let mut config = pkg_config::Config::new(); 22 | config.print_system_libs(false); 23 | 24 | match config.probe("libevdev") { 25 | Ok(lib) => { 26 | // panic if feature 1.10 is enabled and the installed library 27 | // is older than 1.10 28 | #[cfg(feature = "libevdev-1-10")] 29 | { 30 | let (major, minor, patch) = parse_version(&lib.version) 31 | .expect("Could not parse version information"); 32 | assert_eq!(major, 1, "evdev-rs works only with libevdev 1"); 33 | assert!(minor >= 10, 34 | "Feature libevdev-1-10 was enabled, when compiling \ 35 | for a system with libevdev version {}.{}.{}", 36 | major, 37 | minor, 38 | patch, 39 | ); 40 | } 41 | for path in &lib.include_paths { 42 | println!("cargo:include={}", path.display()); 43 | } 44 | return Ok(()); 45 | } 46 | Err(e) => eprintln!( 47 | "Couldn't find libevdev from pkgconfig ({:?}), \ 48 | compiling it from source...", 49 | e 50 | ), 51 | }; 52 | } 53 | 54 | if !Path::new("libevdev/.git").exists() { 55 | let mut download = Command::new("git"); 56 | download.args(&["submodule", "update", "--init", "--depth", "1"]); 57 | run_ignore_error(&mut download)?; 58 | } 59 | 60 | let dst = PathBuf::from(env::var_os("OUT_DIR").unwrap()); 61 | let src = env::current_dir()?; 62 | let mut cp = Command::new("cp"); 63 | cp.arg("-r") 64 | .arg(&src.join("libevdev/")) 65 | .arg(&dst) 66 | .current_dir(&src); 67 | run(&mut cp)?; 68 | 69 | println!("cargo:rustc-link-search={}/lib", dst.display()); 70 | println!("cargo:root={}", dst.display()); 71 | println!("cargo:include={}/include", dst.display()); 72 | println!("cargo:rerun-if-changed=libevdev"); 73 | 74 | println!("cargo:rustc-link-lib=static=evdev"); 75 | let cfg = cc::Build::new(); 76 | let compiler = cfg.get_compiler(); 77 | 78 | if !&dst.join("build").exists() { 79 | fs::create_dir(&dst.join("build"))?; 80 | } 81 | 82 | let mut autogen = Command::new("sh"); 83 | let mut cflags = OsString::new(); 84 | for arg in compiler.args() { 85 | cflags.push(arg); 86 | cflags.push(" "); 87 | } 88 | autogen 89 | .env("CC", compiler.path()) 90 | .env("CFLAGS", cflags) 91 | .current_dir(&dst.join("build")) 92 | .arg( 93 | dst.join("libevdev/autogen.sh") 94 | .to_str() 95 | .unwrap() 96 | .replace("C:\\", "/c/") 97 | .replace("\\", "/"), 98 | ); 99 | if let Ok(h) = env::var("HOST") { 100 | autogen.arg(format!("--host={}", h)); 101 | } 102 | if let Ok(t) = env::var("TARGET") { 103 | autogen.arg(format!("--target={}", t)); 104 | } 105 | autogen.arg(format!("--prefix={}", sanitize_sh(&dst))); 106 | run(&mut autogen)?; 107 | 108 | let mut make = Command::new("make"); 109 | make.arg(&format!("-j{}", env::var("NUM_JOBS").unwrap())) 110 | .current_dir(&dst.join("build")); 111 | run(&mut make)?; 112 | 113 | let mut install = Command::new("make"); 114 | install.arg("install").current_dir(&dst.join("build")); 115 | run(&mut install)?; 116 | Ok(()) 117 | } 118 | 119 | fn run(cmd: &mut Command) -> std::io::Result<()> { 120 | println!("running: {:?}", cmd); 121 | assert!(cmd.status()?.success()); 122 | Ok(()) 123 | } 124 | 125 | fn run_ignore_error(cmd: &mut Command) -> std::io::Result<()> { 126 | println!("running: {:?}", cmd); 127 | let _ = cmd.status(); 128 | Ok(()) 129 | } 130 | 131 | fn sanitize_sh(path: &Path) -> String { 132 | let path = path.to_str().unwrap().replace("\\", "/"); 133 | return change_drive(&path).unwrap_or(path); 134 | 135 | fn change_drive(s: &str) -> Option { 136 | let mut ch = s.chars(); 137 | let drive = ch.next().unwrap_or('C'); 138 | if ch.next() != Some(':') { 139 | return None; 140 | } 141 | if ch.next() != Some('/') { 142 | return None; 143 | } 144 | Some(format!("/{}/{}", drive, &s[drive.len_utf8() + 2..])) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /evdev-rs/rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width=90 -------------------------------------------------------------------------------- /evdev-rs/src/logging.rs: -------------------------------------------------------------------------------- 1 | use evdev_sys as raw; 2 | 3 | pub enum LogPriority { 4 | /// critical errors and application bugs 5 | Error = raw::LIBEVDEV_LOG_ERROR as isize, 6 | /// informational messages 7 | Info = raw::LIBEVDEV_LOG_INFO as isize, 8 | /// debug information 9 | Debug = raw::LIBEVDEV_LOG_DEBUG as isize, 10 | } 11 | 12 | /// Define the minimum level to be printed to the log handler. 13 | /// Messages higher than this level are printed, others are discarded. This 14 | /// is a global setting and applies to any future logging messages. 15 | pub fn set_log_priority(priority: LogPriority) { 16 | unsafe { 17 | raw::libevdev_set_log_priority(priority as i32); 18 | } 19 | } 20 | 21 | /// Return the current log priority level. Messages higher than this level 22 | /// are printed, others are discarded. This is a global setting. 23 | pub fn get_log_priority() -> LogPriority { 24 | unsafe { 25 | let priority = raw::libevdev_get_log_priority(); 26 | match priority { 27 | raw::LIBEVDEV_LOG_ERROR => LogPriority::Error, 28 | raw::LIBEVDEV_LOG_INFO => LogPriority::Info, 29 | raw::LIBEVDEV_LOG_DEBUG => LogPriority::Debug, 30 | c => panic!("unknown log priority: {}", c), 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /evdev-rs/src/macros.rs: -------------------------------------------------------------------------------- 1 | macro_rules! string_getter { 2 | ( $( #[$doc:meta], $func_name:ident, $c_func: ident ),* ) => { 3 | $( 4 | #[$doc] 5 | fn $func_name (&self) -> Option<&str> { 6 | unsafe { 7 | ptr_to_str(raw::$c_func(self.raw())) 8 | } 9 | } 10 | )* 11 | }; 12 | } 13 | 14 | macro_rules! string_setter { 15 | ( $( $func_name:ident, $c_func: ident ),* ) => { 16 | $( 17 | fn $func_name (&self, field: &str) { 18 | let field = CString::new(field).unwrap(); 19 | unsafe { 20 | raw::$c_func(self.raw(), field.as_ptr()) 21 | } 22 | } 23 | )* 24 | }; 25 | } 26 | 27 | macro_rules! product_getter { 28 | ( $( $func_name:ident, $c_func: ident ),* ) => { 29 | $( 30 | fn $func_name (&self) -> u16 { 31 | unsafe { 32 | raw::$c_func(self.raw()) as u16 33 | } 34 | } 35 | )* 36 | }; 37 | } 38 | 39 | macro_rules! product_setter { 40 | ( $( $func_name:ident, $c_func: ident ),* ) => { 41 | $( 42 | fn $func_name (&self, field: u16) { 43 | unsafe { 44 | raw::$c_func(self.raw(), field as c_int); 45 | } 46 | } 47 | )* 48 | }; 49 | } 50 | 51 | macro_rules! abs_getter { 52 | ( $( $func_name:ident, $c_func: ident ),* ) => { 53 | $( 54 | fn $func_name (&self, 55 | code: u32) -> std::io::Result { 56 | let result = unsafe { 57 | raw::$c_func(self.raw(), code as c_uint) as i32 58 | }; 59 | 60 | match result { 61 | 0 => Err(std::io::Error::from_raw_os_error(0)), 62 | k => Ok(k) 63 | } 64 | } 65 | )* 66 | }; 67 | } 68 | 69 | macro_rules! abs_setter { 70 | ( $( $func_name:ident, $c_func: ident ),* ) => { 71 | $( 72 | fn $func_name (&self, 73 | code: u32, 74 | val: i32) { 75 | unsafe { 76 | raw::$c_func(self.raw(), code as c_uint, val as c_int); 77 | } 78 | } 79 | )* 80 | }; 81 | } 82 | -------------------------------------------------------------------------------- /evdev-rs/src/uinput.rs: -------------------------------------------------------------------------------- 1 | use crate::{device::DeviceWrapper, InputEvent}; 2 | use libc::c_int; 3 | use std::io; 4 | use std::os::unix::io::RawFd; 5 | 6 | use crate::util::*; 7 | 8 | use evdev_sys as raw; 9 | 10 | /// Opaque struct representing an evdev uinput device 11 | pub struct UInputDevice { 12 | raw: *mut raw::libevdev_uinput, 13 | } 14 | 15 | unsafe impl Send for UInputDevice {} 16 | 17 | impl UInputDevice { 18 | fn raw(&self) -> *mut raw::libevdev_uinput { 19 | self.raw 20 | } 21 | 22 | /// Create a uinput device based on the given libevdev device. 23 | /// 24 | /// The uinput device will be an exact copy of the libevdev device, minus 25 | /// the bits that uinput doesn't allow to be set. 26 | pub fn create_from_device(device: &T) -> io::Result { 27 | let mut libevdev_uinput = std::ptr::null_mut(); 28 | let result = unsafe { 29 | raw::libevdev_uinput_create_from_device( 30 | device.raw(), 31 | raw::LIBEVDEV_UINPUT_OPEN_MANAGED, 32 | &mut libevdev_uinput, 33 | ) 34 | }; 35 | 36 | match result { 37 | 0 => Ok(UInputDevice { 38 | raw: libevdev_uinput, 39 | }), 40 | error => Err(io::Error::from_raw_os_error(-error)), 41 | } 42 | } 43 | 44 | ///Return the device node representing this uinput device. 45 | /// 46 | /// This relies on `libevdev_uinput_get_syspath()` to provide a valid syspath. 47 | pub fn devnode(&self) -> Option<&str> { 48 | unsafe { ptr_to_str(raw::libevdev_uinput_get_devnode(self.raw())) } 49 | } 50 | 51 | ///Return the syspath representing this uinput device. 52 | /// 53 | /// If the UI_GET_SYSNAME ioctl not available, libevdev makes an educated 54 | /// guess. The UI_GET_SYSNAME ioctl is available since Linux 3.15. 55 | /// 56 | /// The syspath returned is the one of the input node itself 57 | /// (e.g. /sys/devices/virtual/input/input123), not the syspath of the 58 | /// device node returned with libevdev_uinput_get_devnode(). 59 | pub fn syspath(&self) -> Option<&str> { 60 | unsafe { ptr_to_str(raw::libevdev_uinput_get_syspath(self.raw())) } 61 | } 62 | 63 | /// Return the file descriptor used to create this uinput device. 64 | /// 65 | /// This is the fd pointing to /dev/uinput. This file descriptor may be used 66 | /// to write events that are emitted by the uinput device. Closing this file 67 | /// descriptor will destroy the uinput device. 68 | pub fn as_fd(&self) -> Option { 69 | match unsafe { raw::libevdev_uinput_get_fd(self.raw()) } { 70 | 0 => None, 71 | result => Some(result), 72 | } 73 | } 74 | 75 | #[deprecated( 76 | since = "0.5.0", 77 | note = "Prefer `as_fd`. Some function names were changed so they 78 | more closely match their type signature. See issue 42 for discussion 79 | https://github.com/ndesh26/evdev-rs/issues/42" 80 | )] 81 | pub fn fd(&self) -> Option { 82 | self.as_fd() 83 | } 84 | 85 | /// Post an event through the uinput device. 86 | /// 87 | /// It is the caller's responsibility that any event sequence is terminated 88 | /// with an EV_SYN/SYN_REPORT/0 event. Otherwise, listeners on the device 89 | /// node will not see the events until the next EV_SYN event is posted. 90 | pub fn write_event(&self, event: &InputEvent) -> io::Result<()> { 91 | let (ev_type, ev_code) = event_code_to_int(&event.event_code); 92 | let ev_value = event.value as c_int; 93 | 94 | let result = unsafe { 95 | raw::libevdev_uinput_write_event(self.raw(), ev_type, ev_code, ev_value) 96 | }; 97 | 98 | match result { 99 | 0 => Ok(()), 100 | error => Err(io::Error::from_raw_os_error(-error)), 101 | } 102 | } 103 | } 104 | 105 | impl Drop for UInputDevice { 106 | fn drop(&mut self) { 107 | unsafe { 108 | raw::libevdev_uinput_destroy(self.raw()); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /evdev-rs/tests/all.rs: -------------------------------------------------------------------------------- 1 | use evdev_rs::enums::*; 2 | use evdev_rs::*; 3 | use std::fs::File; 4 | use std::os::unix::io::AsRawFd; 5 | 6 | #[test] 7 | fn context_create() { 8 | assert!(UninitDevice::new().is_some()); 9 | } 10 | 11 | #[test] 12 | fn context_create_with_file() { 13 | let f = File::open("/dev/input/event0").unwrap(); 14 | let _d = Device::new_from_file(f).unwrap(); 15 | } 16 | 17 | #[test] 18 | fn context_set_file() { 19 | let d = UninitDevice::new().unwrap(); 20 | let f = File::open("/dev/input/event0").unwrap(); 21 | let _device = d.set_file(f).unwrap(); 22 | } 23 | 24 | #[test] 25 | fn context_change_file() { 26 | let d = UninitDevice::new().unwrap(); 27 | let f1 = File::open("/dev/input/event0").unwrap(); 28 | let f2 = File::open("/dev/input/event0").unwrap(); 29 | let f2_fd = f2.as_raw_fd(); 30 | 31 | let mut d = d.set_file(f1).unwrap(); 32 | d.change_file(f2).unwrap(); 33 | 34 | assert_eq!(d.file().as_raw_fd(), f2_fd); 35 | } 36 | 37 | #[test] 38 | fn context_grab() { 39 | let d = UninitDevice::new().unwrap(); 40 | let f = File::open("/dev/input/event0").unwrap(); 41 | 42 | let mut d = d.set_file(f).unwrap(); 43 | d.grab(GrabMode::Grab).unwrap(); 44 | d.grab(GrabMode::Ungrab).unwrap(); 45 | } 46 | 47 | #[test] 48 | fn device_get_name() { 49 | let d = UninitDevice::new().unwrap(); 50 | 51 | d.set_name("hello"); 52 | assert_eq!(d.name().unwrap(), "hello"); 53 | } 54 | 55 | #[test] 56 | fn device_get_uniq() { 57 | let d = UninitDevice::new().unwrap(); 58 | 59 | d.set_uniq("test"); 60 | assert_eq!(d.uniq().unwrap(), "test"); 61 | } 62 | 63 | #[test] 64 | fn device_get_phys() { 65 | let d = UninitDevice::new().unwrap(); 66 | 67 | d.set_phys("test"); 68 | assert_eq!(d.phys().unwrap(), "test"); 69 | } 70 | 71 | #[test] 72 | fn device_get_product_id() { 73 | let d = UninitDevice::new().unwrap(); 74 | 75 | d.set_product_id(5); 76 | assert_eq!(d.product_id(), 5); 77 | } 78 | 79 | #[test] 80 | fn device_get_vendor_id() { 81 | let d = UninitDevice::new().unwrap(); 82 | 83 | d.set_vendor_id(5); 84 | assert_eq!(d.vendor_id(), 5); 85 | } 86 | 87 | #[test] 88 | fn device_get_bustype() { 89 | let d = UninitDevice::new().unwrap(); 90 | 91 | d.set_bustype(5); 92 | assert_eq!(d.bustype(), 5); 93 | } 94 | 95 | #[test] 96 | fn device_get_version() { 97 | let d = UninitDevice::new().unwrap(); 98 | 99 | d.set_version(5); 100 | assert_eq!(d.version(), 5); 101 | } 102 | 103 | #[test] 104 | fn device_get_absinfo() { 105 | let d = UninitDevice::new().unwrap(); 106 | let f = File::open("/dev/input/event0").unwrap(); 107 | 108 | let d = d.set_file(f).unwrap(); 109 | for code in EventCode::EV_SYN(EV_SYN::SYN_REPORT).iter() { 110 | let absinfo: Option = d.abs_info(&code); 111 | 112 | match absinfo { 113 | None => .., 114 | Some(_a) => .., 115 | }; 116 | } 117 | } 118 | 119 | #[test] 120 | fn device_has_property() { 121 | let d = UninitDevice::new().unwrap(); 122 | let f = File::open("/dev/input/event0").unwrap(); 123 | 124 | let d = d.set_file(f).unwrap(); 125 | for prop in InputProp::INPUT_PROP_POINTER.iter() { 126 | if d.has(&prop) { 127 | panic!("Prop {} is set, shouldn't be", prop); 128 | } 129 | } 130 | } 131 | 132 | #[test] 133 | fn device_has_syn() { 134 | let d = UninitDevice::new().unwrap(); 135 | let f = File::open("/dev/input/event0").unwrap(); 136 | 137 | let d = d.set_file(f).unwrap(); 138 | 139 | assert!(d.has(&EventType::EV_SYN)); // EV_SYN 140 | assert!(d.has(&EventCode::EV_SYN(EV_SYN::SYN_REPORT))); // SYN_REPORT 141 | } 142 | 143 | #[test] 144 | fn device_get_value() { 145 | let d = UninitDevice::new().unwrap(); 146 | let f = File::open("/dev/input/event0").unwrap(); 147 | 148 | let d = d.set_file(f).unwrap(); 149 | 150 | let v2 = d.event_value(&EventCode::EV_SYN(EV_SYN::SYN_REPORT)); // SYN_REPORT 151 | assert_eq!(v2, Some(0)); 152 | } 153 | 154 | #[test] 155 | fn check_event_name() { 156 | assert_eq!("EV_ABS", EventType::EV_ABS.to_string()); 157 | } 158 | 159 | #[test] 160 | fn test_timeval() { 161 | assert_eq!(TimeVal::new(1, 1_000_000), TimeVal::new(2, 0)); 162 | assert_eq!(TimeVal::new(-1, -1_000_000), TimeVal::new(-2, 0)); 163 | assert_eq!(TimeVal::new(1, -1_000_000), TimeVal::new(0, 0)); 164 | assert_eq!(TimeVal::new(-100, 1_000_000 * 100), TimeVal::new(0, 0)); 165 | } 166 | -------------------------------------------------------------------------------- /evdev-rs/tools/make-enums.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eux 4 | 5 | HEADER_DIR=evdev-sys/libevdev/include/linux 6 | 7 | ./tools/make-event-names.py $HEADER_DIR/input-event-codes.h $HEADER_DIR/input.h | head -n -1 > src/enums.rs 8 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | - [hello world](hello_world.py) 4 | Hello world example 5 | 6 | - [a to b](a_to_b.py) 7 | Mapping the 'a' key to 'b' -------------------------------------------------------------------------------- /examples/a_to_b.py: -------------------------------------------------------------------------------- 1 | import map2 2 | 3 | reader = map2.Reader(patterns=[ "/dev/input/by-id/example"]) 4 | mapper = map2.Mapper() 5 | writer = map2.Writer(clone_from = "/dev/input/by-id/example") 6 | 7 | map2.link([reader, mapper, writer]) 8 | 9 | mapper.map("a", "b") 10 | -------------------------------------------------------------------------------- /examples/active_window.py: -------------------------------------------------------------------------------- 1 | import map2 2 | 3 | def on_window_change(active_window_class): 4 | print("active window class: {}".format(active_window_class)) 5 | 6 | 7 | window = map2.Window() 8 | window.on_window_change(on_window_change) -------------------------------------------------------------------------------- /examples/chords.py: -------------------------------------------------------------------------------- 1 | import map2 2 | 3 | reader = map2.Reader(patterns=[ "/dev/input/by-id/example"]) 4 | mapper = map2.ChordMapper() 5 | writer = map2.Writer(clone_from = "/dev/input/by-id/example") 6 | 7 | map2.link([reader, mapper, writer]) 8 | 9 | mapper.map(["a", "b"], "c") 10 | 11 | counter = 0 12 | 13 | def increment(): 14 | global counter 15 | counter += 1 16 | mapper.map(["c", "d"], increment) 17 | -------------------------------------------------------------------------------- /examples/hello_world.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Creates a virtual output keyboard device and types "Hello world!" on it. 3 | ''' 4 | import map2 5 | import time 6 | 7 | map2.default(layout = "us") 8 | 9 | reader = map2.Reader() 10 | writer = map2.Writer(capabilities = {"keys": True}) 11 | 12 | map2.link([reader, writer]) 13 | 14 | reader.send("Hello world!") 15 | 16 | # keep running for 1sec so the event can be processed 17 | time.sleep(1) 18 | -------------------------------------------------------------------------------- /examples/keyboard_to_controller.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | ''' 3 | Maps the keyboard to a virtual controller. 4 | 5 | WASD keys -> left joystick 6 | IHJK keys -> right joystick 7 | TFGH keys -> dpad 8 | arrow keys -> A/B/X/Y 9 | q key -> left shoulder 10 | e key -> left shoulder 2 11 | u key -> right shoulder 12 | o key -> right shoulder 2 13 | x key -> left joystick click 14 | m key -> left joystick click 15 | left shift -> select 16 | right shift -> start 17 | spacebar -> exit 18 | ''' 19 | import map2 20 | 21 | map2.default(layout = "us") 22 | 23 | reader = map2.Reader(patterns=["/dev/input/by-id/example-keyboard"]) 24 | 25 | mapper = map2.Mapper() 26 | 27 | controller = map2.Writer(name="virtual-controller", capabilities = { 28 | "buttons": True, 29 | "abs": { 30 | # map joysticks to [0..255] 31 | "X": {"value": 128, "min": 0, "max": 255}, 32 | "Y": {"value": 128, "min": 0, "max": 255}, 33 | "RX": {"value": 128, "min": 0, "max": 255}, 34 | "RY": {"value": 128, "min": 0, "max": 255}, 35 | # map dpad to [-1..1] 36 | "hat0X": {"value": 0, "min": -1, "max": 1}, 37 | "hat0Y": {"value": 0, "min": -1, "max": 1}, 38 | } 39 | }) 40 | 41 | map2.link([reader, mapper, controller]) 42 | 43 | 44 | # some convenience functions 45 | def joystick(axis, offset): 46 | def fn(): 47 | # the joystick range is [0..255], so 128 is neutral 48 | print([axis, offset]) 49 | controller.send("{absolute "+axis+" "+str(128 + offset)+"}") 50 | return fn 51 | 52 | def dpad(axis, offset): 53 | def fn(): 54 | controller.send("{absolute "+axis+" "+str(offset)+"}") 55 | return fn 56 | 57 | def button(button, state): 58 | def fn(): 59 | controller.send("{"+button+" "+state+"}") 60 | return fn 61 | 62 | 63 | # WASD directional keys to the left joystick 64 | mapper.map("w down", joystick("Y", -80)) 65 | mapper.map("w up", joystick("Y", 0)) 66 | mapper.nop("w repeat") 67 | 68 | mapper.map("a down", joystick("X", -80)) 69 | mapper.map("a up", joystick("X", 0)) 70 | mapper.nop("a repeat") 71 | 72 | mapper.map("s down", joystick("Y", 80)) 73 | mapper.map("s up", joystick("Y", 0)) 74 | mapper.nop("s repeat") 75 | 76 | mapper.map("d down", joystick("X", 80)) 77 | mapper.map("d up", joystick("X", 0)) 78 | mapper.nop("d repeat") 79 | 80 | # map WASD directional keys to the right joystick 81 | mapper.map("i down", joystick("RY", -80)) 82 | mapper.map("i up", joystick("RY", 0)) 83 | mapper.nop("i repeat") 84 | 85 | mapper.map("j down", joystick("RX", -80)) 86 | mapper.map("j up", joystick("RX", 0)) 87 | mapper.nop("j repeat") 88 | 89 | mapper.map("k down", joystick("RY", 80)) 90 | mapper.map("k up", joystick("RY", 0)) 91 | mapper.nop("k repeat") 92 | 93 | mapper.map("l down", joystick("RX", 80)) 94 | mapper.map("l up", joystick("RX", 0)) 95 | mapper.nop("l repeat") 96 | 97 | # TFGH directional keys to the left joystick 98 | mapper.map("t down", dpad("hat0Y", -1)) 99 | mapper.map("t up", dpad("hat0Y", 0)) 100 | mapper.nop("t repeat") 101 | 102 | mapper.map("f down", dpad("hat0X", -1)) 103 | mapper.map("f up", dpad("hat0x", 0)) 104 | mapper.nop("f repeat") 105 | 106 | mapper.map("g down", dpad("hat0Y", 1)) 107 | mapper.map("g up", dpad("hat0Y", 0)) 108 | mapper.nop("g repeat") 109 | 110 | mapper.map("h down", dpad("hat0X", 1)) 111 | mapper.map("h up", dpad("hat0X", 0)) 112 | mapper.nop("h repeat") 113 | 114 | # A/B/X/Y buttons (or whatever other naming) 115 | mapper.map("up", "{btn_north}") 116 | mapper.map("down", "{btn_south}") 117 | mapper.map("left", "{btn_west}") 118 | mapper.map("right", "{btn_east}") 119 | 120 | # left shoulder buttons 121 | mapper.map("q", "{btn_tl}") 122 | mapper.map("e", "{btn_tl2}") 123 | 124 | # right shoulder buttons 125 | mapper.map("u", "{btn_tr}") 126 | mapper.map("o", "{btn_tr2}") 127 | 128 | # start/select buttons 129 | mapper.map("left_shift", "{btn_select}") 130 | mapper.map("right_shift", "{btn_start}") 131 | 132 | # joystick buttons 133 | mapper.map("x", "{btn_thumbl}") 134 | mapper.map("m", "{btn_thumbr}") 135 | 136 | # exit wtih space 137 | mapper.map("space", lambda: map2.exit()) 138 | 139 | 140 | # keep running 141 | map2.wait() 142 | -------------------------------------------------------------------------------- /examples/tests/_setup_integration_tests/setup_integration_tests.rs: -------------------------------------------------------------------------------- 1 | #![feature(internal_output_capture)] 2 | 3 | use std::io::Write; 4 | use std::thread; 5 | use std::time::Duration; 6 | 7 | use map2::python::*; 8 | use map2::*; 9 | 10 | #[pyo3_asyncio::tokio::main] 11 | async fn main() -> pyo3::PyResult<()> { 12 | let cmd = std::process::Command::new("maturin") 13 | .arg("dev") 14 | // .arg("--") 15 | // .arg("--cfg").arg("test") 16 | // .arg("--cfg").arg("integration") 17 | .arg("--features") 18 | .arg("integration") 19 | .output()?; 20 | 21 | if !cmd.status.success() { 22 | std::io::stderr().write(&cmd.stderr)?; 23 | std::process::exit(1); 24 | } 25 | 26 | pyo3_asyncio::testing::main().await 27 | } 28 | 29 | #[path = "../"] 30 | mod integration_tests { 31 | automod::dir!("examples/tests"); 32 | } 33 | 34 | pub fn writer_read(py: Python, module: &PyModule, name: &str) -> Option { 35 | let target = module.getattr(name).unwrap().to_object(py); 36 | 37 | target 38 | .call_method0(py, "__test__read_ev") 39 | .unwrap() 40 | .extract::>(py) 41 | .unwrap() 42 | .and_then(|x| serde_json::from_str(&x).unwrap()) 43 | } 44 | 45 | pub fn writer_read_all(py: Python, module: &PyModule, name: &str) -> Vec { 46 | let mut acc = vec![]; 47 | while let Some(ev) = writer_read(py, module, name) { 48 | acc.push(ev); 49 | } 50 | acc 51 | } 52 | 53 | pub fn reader_send(py: Python, module: &PyModule, name: &str, ev: &EvdevInputEvent) { 54 | let target = module.getattr(name).unwrap().to_object(py); 55 | let ev = serde_json::to_string(ev).unwrap(); 56 | 57 | target.call_method(py, "__test__write_ev", (ev,), None).unwrap(); 58 | } 59 | 60 | pub fn sleep(py: Python, millis: u64) { 61 | py.allow_threads(|| { 62 | thread::sleep(Duration::from_millis(millis)); 63 | }); 64 | } 65 | 66 | #[macro_export] 67 | macro_rules! assert_keys { 68 | ($py: expr, $m: expr, $name: expr, $input: expr) => { 69 | assert_eq!(writer_read_all($py, $m, $name), keys($input),); 70 | }; 71 | } 72 | 73 | #[macro_export] 74 | macro_rules! assert_empty { 75 | ($py: expr, $module: expr, $name: expr) => { 76 | assert_eq!(writer_read_all($py, $module, $name), vec![]); 77 | }; 78 | } 79 | 80 | pub fn reader_send_all(py: Python, module: &PyModule, name: &str, ev_list: &Vec) { 81 | let target = module.getattr(name).unwrap().to_object(py); 82 | 83 | for ev in ev_list.iter() { 84 | let ev = serde_json::to_string(ev).unwrap(); 85 | target.call_method(py, "__test__write_ev", (ev,), None).unwrap(); 86 | } 87 | } 88 | 89 | pub fn keys(input: &str) -> Vec { 90 | parse_key_sequence(input, Some(&Default::default())).unwrap().to_input_ev() 91 | } 92 | 93 | #[macro_export] 94 | macro_rules! send { 95 | ($reader: expr, $keys: expr) => { 96 | reader_send_all(py, m, $reader, &keys($keys)); 97 | }; 98 | } 99 | 100 | #[macro_export] 101 | macro_rules! assert_output { 102 | ($writer: expr, $keys: expr) => { 103 | assert_eq!(writer_read_all(py, m, $writer), keys($keys),); 104 | }; 105 | } 106 | 107 | #[macro_export] 108 | macro_rules! sleep { 109 | ($millis: expr) => {}; 110 | } 111 | -------------------------------------------------------------------------------- /examples/tests/a_to_b.rs: -------------------------------------------------------------------------------- 1 | use std::thread; 2 | use std::time::Duration; 3 | 4 | use crate::*; 5 | 6 | #[pyo3_asyncio::tokio::test] 7 | async fn a_to_b() -> PyResult<()> { 8 | Python::with_gil(|py| -> PyResult<()> { 9 | let m = pytests::include_python!(); 10 | 11 | reader_send_all(py, m, "reader", &keys("a")); 12 | 13 | py.allow_threads(|| { 14 | thread::sleep(Duration::from_millis(25)); 15 | }); 16 | 17 | assert_eq!(writer_read_all(py, m, "writer"), keys("b"),); 18 | 19 | Ok(()) 20 | })?; 21 | Ok(()) 22 | } 23 | -------------------------------------------------------------------------------- /examples/tests/chords.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | const READER: &str = "reader"; 4 | const WRITER: &str = "writer"; 5 | 6 | #[pyo3_asyncio::tokio::test] 7 | async fn single_key_click() -> PyResult<()> { 8 | Python::with_gil(|py| -> PyResult<()> { 9 | let m = pytests::include_python!(); 10 | 11 | reader_send_all(py, m, READER, &keys("a")); 12 | sleep(py, 55); 13 | assert_keys!(py, m, WRITER, "a"); 14 | 15 | reader_send_all(py, m, READER, &keys("b")); 16 | sleep(py, 55); 17 | assert_keys!(py, m, WRITER, "b"); 18 | 19 | Ok(()) 20 | })?; 21 | Ok(()) 22 | } 23 | 24 | #[pyo3_asyncio::tokio::test] 25 | async fn hold_key() -> PyResult<()> { 26 | Python::with_gil(|py| -> PyResult<()> { 27 | let m = pytests::include_python!(); 28 | 29 | reader_send_all(py, m, READER, &keys("{a down}")); 30 | sleep(py, 55); 31 | reader_send_all(py, m, READER, &keys("{a repeat}{a up}")); 32 | sleep(py, 10); 33 | assert_keys!(py, m, WRITER, "{a down}{a repeat}{a up}"); 34 | sleep(py, 55); 35 | assert_empty!(py, m, WRITER); 36 | 37 | Ok(()) 38 | })?; 39 | Ok(()) 40 | } 41 | 42 | #[pyo3_asyncio::tokio::test] 43 | async fn break_chord() -> PyResult<()> { 44 | Python::with_gil(|py| -> PyResult<()> { 45 | let m = pytests::include_python!(); 46 | 47 | reader_send_all(py, m, READER, &keys("{a down}")); 48 | sleep(py, 10); 49 | reader_send_all(py, m, READER, &keys("{z down}")); 50 | sleep(py, 10); 51 | assert_keys!(py, m, WRITER, "a{z down}"); 52 | reader_send_all(py, m, READER, &keys("{a up}{z up}")); 53 | sleep(py, 10); 54 | assert_keys!(py, m, WRITER, "{z up}"); 55 | 56 | Ok(()) 57 | })?; 58 | Ok(()) 59 | } 60 | 61 | #[pyo3_asyncio::tokio::test] 62 | async fn simple_chord() -> PyResult<()> { 63 | Python::with_gil(|py| -> PyResult<()> { 64 | let m = pytests::include_python!(); 65 | 66 | reader_send_all(py, m, READER, &keys("{a down}{b down}{a up}{b up}")); 67 | sleep(py, 55); 68 | assert_eq!(writer_read_all(py, m, WRITER), keys("c"),); 69 | sleep(py, 55); 70 | assert_empty!(py, m, WRITER); 71 | 72 | Ok(()) 73 | })?; 74 | Ok(()) 75 | } 76 | 77 | #[pyo3_asyncio::tokio::test] 78 | async fn multi_chord() -> PyResult<()> { 79 | Python::with_gil(|py| -> PyResult<()> { 80 | let m = pytests::include_python!(); 81 | 82 | reader_send_all(py, m, "reader", &keys("{a down}{b down}{b up}{b down}{a up}{b up}")); 83 | sleep(py, 55); 84 | assert_eq!(writer_read_all(py, m, WRITER), keys("cc"),); 85 | 86 | Ok(()) 87 | })?; 88 | Ok(()) 89 | } 90 | 91 | #[pyo3_asyncio::tokio::test] 92 | async fn chord_to_function() -> PyResult<()> { 93 | Python::with_gil(|py| -> PyResult<()> { 94 | let m = pytests::include_python!(); 95 | 96 | let counter = m.getattr("counter").unwrap().extract::().unwrap(); 97 | assert_eq!(counter, 0); 98 | 99 | reader_send_all(py, m, READER, &keys("{c down}{d down}{c up}{d up}")); 100 | sleep(py, 55); 101 | assert_empty!(py, m, WRITER); 102 | 103 | let counter = m.getattr("counter").unwrap().extract::().unwrap(); 104 | assert_eq!(counter, 1); 105 | 106 | Ok(()) 107 | })?; 108 | Ok(()) 109 | } 110 | -------------------------------------------------------------------------------- /examples/tests/hello_world.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[pyo3_asyncio::tokio::test] 4 | async fn hello_world() -> PyResult<()> { 5 | Python::with_gil(|py| -> PyResult<()> { 6 | let m = pytests::include_python!(); 7 | 8 | assert_eq!(writer_read_all(py, m, "writer"), keys("Hello world!"),); 9 | 10 | Ok(()) 11 | })?; 12 | Ok(()) 13 | } 14 | -------------------------------------------------------------------------------- /examples/tests/text_mapping.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | const READER: &str = "reader"; 4 | const WRITER: &str = "writer"; 5 | 6 | #[pyo3_asyncio::tokio::test] 7 | async fn passes_through_unrealated_sequences() -> PyResult<()> { 8 | Python::with_gil(|py| -> PyResult<()> { 9 | let m = pytests::include_python!(); 10 | 11 | reader_send_all(py, m, READER, &keys("hellp")); 12 | sleep(py, 5); 13 | assert_keys!(py, m, WRITER, "hellp"); 14 | sleep(py, 5); 15 | assert_empty!(py, m, WRITER); 16 | 17 | Ok(()) 18 | })?; 19 | Ok(()) 20 | } 21 | 22 | #[pyo3_asyncio::tokio::test] 23 | async fn hold_key() -> PyResult<()> { 24 | Python::with_gil(|py| -> PyResult<()> { 25 | let m = pytests::include_python!(); 26 | 27 | reader_send_all(py, m, READER, &keys("hello")); 28 | sleep(py, 5); 29 | let mut output = "hello".to_owned(); 30 | for _ in 0..("hello").len() { 31 | output.push_str("{backspace}"); 32 | } 33 | output.push_str("bye"); 34 | assert_keys!(py, m, WRITER, &output); 35 | sleep(py, 5); 36 | assert_empty!(py, m, WRITER); 37 | 38 | Ok(()) 39 | })?; 40 | Ok(()) 41 | } 42 | 43 | #[pyo3_asyncio::tokio::test] 44 | async fn map_to_function() -> PyResult<()> { 45 | Python::with_gil(|py| -> PyResult<()> { 46 | let m = pytests::include_python!(); 47 | 48 | let counter = m.getattr("counter").unwrap().extract::().unwrap(); 49 | assert_eq!(counter, 0); 50 | 51 | reader_send_all(py, m, READER, &keys("Something")); 52 | sleep(py, 5); 53 | 54 | let mut output = "Something".to_owned(); 55 | for _ in 0..("Something").len() { 56 | output.push_str("{backspace}"); 57 | } 58 | assert_keys!(py, m, WRITER, &output); 59 | 60 | sleep(py, 100); 61 | let counter = m.getattr("counter").unwrap().extract::().unwrap(); 62 | assert_eq!(counter, 1); 63 | 64 | Ok(()) 65 | })?; 66 | Ok(()) 67 | } 68 | 69 | #[pyo3_asyncio::tokio::test] 70 | async fn capital_leters() -> PyResult<()> { 71 | Python::with_gil(|py| -> PyResult<()> { 72 | let m = pytests::include_python!(); 73 | 74 | reader_send_all(py, m, READER, &keys("LaSeRs")); 75 | sleep(py, 5); 76 | let mut output = "LaSeRs".to_owned(); 77 | for _ in 0..("LaSeRs").len() { 78 | output.push_str("{backspace}"); 79 | } 80 | output.push_str("lAsErS"); 81 | assert_keys!(py, m, WRITER, &output); 82 | 83 | Ok(()) 84 | })?; 85 | Ok(()) 86 | } 87 | -------------------------------------------------------------------------------- /examples/tests/wasd_mouse_control.rs: -------------------------------------------------------------------------------- 1 | use std::thread; 2 | use std::time::Duration; 3 | 4 | use evdev_rs::enums::EventCode; 5 | 6 | use map2::key_primitives::Key; 7 | 8 | use crate::*; 9 | 10 | #[pyo3_asyncio::tokio::test] 11 | async fn wasd_mouse_control() -> PyResult<()> { 12 | Python::with_gil(|py| -> PyResult<()> { 13 | let m = pytests::include_python!(); 14 | 15 | reader_send(py, m, "reader_kbd", &Key::from_str("w").unwrap().to_input_ev(1)); 16 | 17 | // sleep for long enough to trigger the timeout once 18 | py.allow_threads(|| { 19 | thread::sleep(Duration::from_millis(25)); 20 | }); 21 | 22 | reader_send(py, m, "reader_kbd", &Key::from_str("w").unwrap().to_input_ev(0)); 23 | 24 | assert_eq!( 25 | writer_read_all(py, m, "writer_mouse"), 26 | vec![ 27 | EvdevInputEvent::new(&Default::default(), &EventCode::EV_REL(REL_Y), -15), 28 | EvdevInputEvent::new(&Default::default(), &EventCode::EV_REL(REL_Y), -15), 29 | ] 30 | ); 31 | 32 | Ok(()) 33 | })?; 34 | Ok(()) 35 | } 36 | 37 | -------------------------------------------------------------------------------- /examples/text_mapping.py: -------------------------------------------------------------------------------- 1 | import map2 2 | 3 | reader = map2.Reader(patterns=[ "/dev/input/by-id/example"]) 4 | mapper = map2.TextMapper() 5 | writer = map2.Writer(clone_from = "/dev/input/by-id/example") 6 | 7 | map2.link([reader, mapper, writer]) 8 | 9 | mapper.map("hello", "bye") 10 | 11 | mapper.map("LaSeRs", "lAsErS") 12 | 13 | counter = 0 14 | 15 | def increment(): 16 | global counter 17 | counter += 1 18 | mapper.map("Something", increment) 19 | -------------------------------------------------------------------------------- /examples/wasd_mouse_control.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Move the mouse using the 'w', 'a', 's', 'd' directional keys. 3 | ''' 4 | 5 | import map2 6 | import time 7 | import threading 8 | 9 | map2.default(layout="us") 10 | 11 | # an easy to use interval utility that allows running functions on a timer 12 | class setInterval: 13 | def __init__(self, interval, action): 14 | self.interval = interval / 1000 15 | self.action = action 16 | self.stopEvent = threading.Event() 17 | thread = threading.Thread(target=self.__setInterval) 18 | thread.start() 19 | 20 | def __setInterval(self): 21 | nextTime = time.time() + self.interval 22 | while not self.stopEvent.wait(nextTime - time.time()): 23 | nextTime += self.interval 24 | self.action() 25 | 26 | def cancel(self): 27 | self.stopEvent.set() 28 | 29 | 30 | # read from keyboard 31 | reader_kbd = map2.Reader(patterns=["/dev/input/by-id/example-keyboard"]) 32 | 33 | # to move the mouse programmatically, we need a mouse reader we can write into 34 | reader_mouse = map2.Reader() 35 | 36 | # add new virtual output devices 37 | writer_kbd = map2.Writer(clone_from="/dev/input/by-id/example-keyboard") 38 | writer_mouse = map2.Writer(capabilities={"rel": True, "buttons": True}) 39 | 40 | # add mapper 41 | mapper_kbd = map2.Mapper() 42 | 43 | # setup the event routing 44 | map2.link([reader_kbd, mapper_kbd, writer_kbd]) 45 | map2.link([reader_mouse, writer_mouse]) 46 | 47 | 48 | # we keep a map of intervals that maps each key to the associated interval 49 | intervals = {} 50 | 51 | 52 | def mouse_ctrl(key, state, axis, multiplier): 53 | def inner_fn(): 54 | # on key release, remove and cancel the corresponding interval 55 | if state == 0: 56 | if key in intervals: 57 | intervals.pop(key).cancel() 58 | return 59 | 60 | # this function will move our mouse using the virtual reader 61 | def send(): 62 | value = 15 * multiplier 63 | reader_mouse.send("{{relative {} {}}}".format(axis, value)) 64 | 65 | # we call it once to move the mouse a bit immediately on key down 66 | send() 67 | # and register an interval that will continue to move it on a timer 68 | intervals[key] = setInterval(20, send) 69 | return inner_fn 70 | 71 | 72 | # setup the key mappings 73 | mapper_kbd.map("w up", mouse_ctrl("w", 0, "Y", -1)) 74 | mapper_kbd.map("w down", mouse_ctrl("w", 1, "Y", -1)) 75 | mapper_kbd.nop("w repeat") 76 | 77 | mapper_kbd.map("a up", mouse_ctrl("a", 0, "X", -1)) 78 | mapper_kbd.map("a down", mouse_ctrl("a", 1, "X", -1)) 79 | mapper_kbd.nop("a repeat") 80 | 81 | mapper_kbd.map("s up", mouse_ctrl("s", 0, "Y", 1)) 82 | mapper_kbd.map("s down", mouse_ctrl("s", 1, "Y", 1)) 83 | mapper_kbd.nop("s repeat") 84 | 85 | mapper_kbd.map("d up", mouse_ctrl("d", 0, "X", 1)) 86 | mapper_kbd.map("d down", mouse_ctrl("d", 1, "X", 1)) 87 | mapper_kbd.nop("d repeat") 88 | 89 | 90 | # Keep running forever 91 | map2.wait() 92 | -------------------------------------------------------------------------------- /package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | filename="map2-$(git describe --abbrev=0 --tags)" 3 | arch="$(uname -m)" 4 | 5 | tar -cvzf "$filename-$arch.tar.gz" -C pkg/map2 --exclude='.[^/]*' . 6 | -------------------------------------------------------------------------------- /pytests/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "pytests" 7 | version = "1.0.0" 8 | -------------------------------------------------------------------------------- /pytests/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pytests" 3 | version = "1.0.0" 4 | authors = ["shiro "] 5 | edition = "2018" 6 | 7 | [lib] 8 | name = "pytests" 9 | crate-type = ["proc-macro"] 10 | proc-macro = true 11 | -------------------------------------------------------------------------------- /pytests/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(proc_macro_span)] 2 | 3 | #[proc_macro] 4 | pub fn include_python(_item: proc_macro::TokenStream) -> proc_macro::TokenStream { 5 | let span = proc_macro::Span::call_site(); 6 | let source = span.source_file(); 7 | 8 | let py_filename = format!("{}.py", source.path().file_stem().unwrap().to_string_lossy()); 9 | 10 | format!("PyModule::from_code(py, include_str!(\"../{py_filename}\"), \"\", \"\")?").parse().unwrap() 11 | } 12 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | version="$1" 5 | shift 6 | 7 | if [ -z "$version" ]; then 8 | echo "usage: release.sh version" 9 | exit 1 10 | fi 11 | 12 | sed -i -re 's/^version = ".*/version = "'"$version"'"/' Cargo.toml 13 | sed -i -re '/^name = "map2"/{n;s/^version = ".*/version = "'"$version"'"/}' Cargo.lock 14 | git reset 15 | git add Cargo.{lock,toml} 16 | git commit . -m "$version" 17 | git push 18 | git tag -a "$version" -m "$version" 19 | git push origin "$version" 20 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 120 2 | use_small_heuristics = "Max" 3 | -------------------------------------------------------------------------------- /scripts/arch/.SRCINFO: -------------------------------------------------------------------------------- 1 | pkgbase = map2 2 | pkgdesc = A scripting language that allows complex key remapping on Linux, written in Rust 3 | pkgver = 1.0.0 4 | pkgrel = 1 5 | arch = x86_64 6 | arch = i686 7 | license = MIT 8 | makedepends = rustup 9 | source = git+https://github.com/shiro/map2.git 10 | sha256sums = SKIP 11 | 12 | pkgname = map2 13 | 14 | -------------------------------------------------------------------------------- /scripts/arch/PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: shiro 2 | 3 | pkgname=map2 4 | pkgver=1.0.0 5 | pkgrel=1 6 | pkgdesc="A scripting language that allows complex key remapping on Linux, written in Rust" 7 | arch=('x86_64' 'i686') 8 | license=('MIT') 9 | depends=() 10 | makedepends=(rustup) 11 | source=("git+https://github.com/shiro/${pkgname%-git}.git") 12 | sha256sums=('SKIP') 13 | 14 | build() { 15 | cd "$pkgname" 16 | cargo build --release --locked --all-features --target-dir=target 17 | } 18 | 19 | check() { 20 | cd "$pkgname" 21 | cargo test --release --locked --target-dir=target 22 | } 23 | 24 | package() { 25 | cd "$pkgname" 26 | install -Dm 755 target/release/${pkgname} -t "${pkgdir}/usr/bin" 27 | 28 | install -Dm644 docs/man/map2.1 "$pkgdir/usr/share/man/man1/map2.1" 29 | } 30 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import { } }: 2 | 3 | pkgs.mkShell { 4 | buildInputs = with pkgs; [ libevdev udev libcap ]; 5 | nativeBuildInputs = with pkgs; [ pkg-config libxkbcommon ]; 6 | packages = with pkgs; [ 7 | automake 8 | autoconf 9 | automake 10 | ]; 11 | 12 | shellHook = '' 13 | export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:${ 14 | with pkgs; pkgs.lib.makeLibraryPath [ libevdev udev libcap ] 15 | }" 16 | ''; 17 | } 18 | -------------------------------------------------------------------------------- /src/capabilities/mod.rs: -------------------------------------------------------------------------------- 1 | use serde::*; 2 | 3 | use crate::*; 4 | 5 | #[derive(Debug, Clone, Serialize, Deserialize)] 6 | pub struct AbsInfo { 7 | #[serde(default)] 8 | pub value: i32, 9 | #[serde(default)] 10 | #[serde(rename(deserialize = "min"))] 11 | pub minimum: i32, 12 | #[serde(default)] 13 | #[serde(rename(deserialize = "max"))] 14 | pub maximum: i32, 15 | #[serde(default)] 16 | pub fuzz: i32, 17 | #[serde(default)] 18 | pub flat: i32, 19 | #[serde(default)] 20 | pub resolution: i32, 21 | } 22 | 23 | impl AbsInfo { 24 | pub fn into_evdev(self) -> evdev_rs::AbsInfo { 25 | evdev_rs::AbsInfo { 26 | value: self.value, 27 | minimum: self.minimum, 28 | maximum: self.maximum, 29 | fuzz: self.fuzz, 30 | flat: self.flat, 31 | resolution: self.resolution, 32 | } 33 | } 34 | } 35 | 36 | #[derive(Debug, Serialize, Deserialize)] 37 | #[serde(untagged)] 38 | pub enum AbsSpec { 39 | Bool(bool), 40 | AbsInfo(AbsInfo), 41 | } 42 | 43 | #[derive(Debug, Serialize, Deserialize)] 44 | #[serde(untagged)] 45 | pub enum Abs { 46 | Bool(bool), 47 | Specification(HashMap), 48 | } 49 | 50 | impl Default for Abs { 51 | fn default() -> Self { 52 | Self::Bool(false) 53 | } 54 | } 55 | 56 | #[derive(Debug, Serialize, Deserialize)] 57 | pub struct Capabilities { 58 | #[serde(default)] 59 | pub rel: bool, 60 | #[serde(default)] 61 | pub abs: Abs, 62 | #[serde(default)] 63 | pub keys: bool, 64 | #[serde(default)] 65 | pub buttons: bool, 66 | } 67 | -------------------------------------------------------------------------------- /src/closure_channel.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Error, Result}; 2 | use futures_time::prelude::*; 3 | use tokio::sync::mpsc::{channel, Receiver, Sender}; 4 | 5 | pub struct ClosureChannel { 6 | tx: Sender>, 7 | } 8 | 9 | impl Clone for ClosureChannel { 10 | fn clone(&self) -> Self { 11 | Self { tx: self.tx.clone() } 12 | } 13 | } 14 | 15 | impl ClosureChannel { 16 | pub fn new() -> (Self, Receiver>) { 17 | let (mut tx, mut rx) = channel(64); 18 | (Self { tx }, rx) 19 | } 20 | 21 | pub fn call<'a, Ret: Send + 'a>(&self, closure: Box Ret + Send + 'a>) -> Result { 22 | futures::executor::block_on(self.call_async(closure)) 23 | } 24 | 25 | pub async fn call_async<'a, Ret: Send + 'a>( 26 | &self, 27 | closure: Box Ret + Send + 'a>, 28 | ) -> Result { 29 | let (mut tx, mut rx) = tokio::sync::oneshot::channel::(); 30 | 31 | let cb = Box::new(move |value: &mut Value| { 32 | let ret = closure(value); 33 | tx.send(ret).map_err(|err| anyhow!("failed to send return message")).unwrap(); 34 | }); 35 | 36 | // we guarantee the lifetimes are compatible 37 | let cb = unsafe { 38 | std::mem::transmute::, Box>( 39 | cb, 40 | ) 41 | }; 42 | 43 | self.tx.try_send(cb).map_err(|err| anyhow!("closure channel error: failed to send message"))?; 44 | 45 | match rx.timeout(futures_time::time::Duration::from_millis(5000)).await { 46 | Ok(ret) => match ret { 47 | Ok(ret) => Ok(ret), 48 | Err(err) => Err(anyhow!("closure channel error: other side already closed")), 49 | }, 50 | Err(_) => Err(anyhow!("closure channel timed out, probably due to a deadlock")), 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/device/device_logging.rs: -------------------------------------------------------------------------------- 1 | use evdev_rs::enums::EventCode; 2 | use evdev_rs::InputEvent; 3 | 4 | pub fn print_event_debug(ev: &InputEvent) { 5 | match ev.event_code { 6 | EventCode::EV_SYN(_) => println!( 7 | "Event: time {}.{}, ++++++++++++++++++++ {} +++++++++++++++", 8 | ev.time.tv_sec, 9 | ev.time.tv_usec, 10 | ev.event_type().unwrap() 11 | ), 12 | _ => println!( 13 | "Event: time {}.{}, type {} , code {} , value {}", 14 | ev.time.tv_sec, 15 | ev.time.tv_usec, 16 | ev.event_type().map(|ev_type| format!("{}", ev_type)).unwrap_or("None".to_owned()), 17 | ev.event_code, 18 | ev.value 19 | ), 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/device/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod device_logging; 2 | pub(crate) mod virt_device; 3 | pub mod virtual_input_device; 4 | pub mod virtual_output_device; 5 | -------------------------------------------------------------------------------- /src/device/virtual_output_device.rs: -------------------------------------------------------------------------------- 1 | use evdev_rs::{UInputDevice, UninitDevice}; 2 | 3 | use crate::device::virt_device::DeviceCapabilities; 4 | use crate::*; 5 | 6 | use super::*; 7 | 8 | pub struct VirtualOutputDevice { 9 | output_device: UInputDevice, 10 | } 11 | 12 | impl VirtualOutputDevice { 13 | pub fn send(&mut self, ev: &EvdevInputEvent) -> Result<()> { 14 | self.output_device.write_event(&ev).map_err(|err| anyhow!("failed to write event into uinput device: {}", err)) 15 | } 16 | } 17 | 18 | pub enum DeviceInitPolicy { 19 | NewDevice(String, DeviceCapabilities), 20 | CloneExistingDevice(String), 21 | } 22 | 23 | pub fn init_virtual_output_device(init_policy: &DeviceInitPolicy) -> Result { 24 | let mut new_device = UninitDevice::new() 25 | .ok_or(anyhow!("failed to instantiate udev device: libevdev didn't return a device"))? 26 | .unstable_force_init(); 27 | 28 | match init_policy { 29 | DeviceInitPolicy::NewDevice(name, capabilities) => { 30 | virt_device::init_virtual_device(&mut new_device, name, capabilities) 31 | .map_err(|err| anyhow!("failed to instantiate udev device: {}", err))?; 32 | } 33 | DeviceInitPolicy::CloneExistingDevice(existing_device_fd_path) => { 34 | virt_device::clone_virtual_device(&mut new_device, existing_device_fd_path) 35 | .map_err(|err| anyhow!("failed to clone existing udev device: {}", err))?; 36 | } 37 | } 38 | 39 | let input_device = UInputDevice::create_from_device(&new_device); 40 | 41 | if let Err(err) = &input_device { 42 | if err.kind() == io::ErrorKind::PermissionDenied { 43 | return Err(anyhow!("failed to obtain write access to '/dev/uinput': {}", err)); 44 | } 45 | }; 46 | 47 | let output_device = input_device.map_err(|err| anyhow!("failed to initialize uinput device: {}", err))?; 48 | 49 | Ok(VirtualOutputDevice { output_device }) 50 | } 51 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | use crate::python::*; 4 | 5 | #[derive(Error, Debug)] 6 | pub enum ApplicationError { 7 | #[error("[UNSUPPORTED_PLATFORM] unsupported platform, supported platforms are: Hyprland, X11")] 8 | UnsupportedPlatform, 9 | #[error("[KEY_PARSE] invalid key:\n{0}")] 10 | KeyParse(String), 11 | #[error("[KEY_SEQ_PARSE] invalid key sequence:\n{0}")] 12 | KeySequenceParse(String), 13 | #[error("[INVALID_LINK_TARGET] invalid link target")] 14 | InvalidLinkTarget, 15 | #[error("[NOT_CALLABLE] expected a callable object (i.e. a function)")] 16 | NotCallable, 17 | #[error("[INVALID_INPUT_TYPE] expected input to be of type {type_}")] 18 | InvalidInputType { type_: String }, 19 | #[error("[UNEXPECTED_NON_BUTTON_INPUT] expected only button inputs")] 20 | NonButton, 21 | #[error("can't keep up with event processing, dropping events!")] 22 | TooManyEvents, 23 | } 24 | 25 | impl From for PyErr { 26 | fn from(value: ApplicationError) -> Self { 27 | PyRuntimeError::new_err(value.to_string()) 28 | } 29 | } 30 | 31 | impl ApplicationError { 32 | pub fn into_py(self) -> PyErr { 33 | self.into() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/event.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[derive(Debug, Clone)] 4 | pub enum InputEvent { 5 | Raw(EvdevInputEvent), 6 | } 7 | -------------------------------------------------------------------------------- /src/event_handlers.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | pub(crate) fn update_modifiers(modifiers: &mut Arc, action: &KeyAction) -> bool { 4 | // TODO find a way to do this with a single accessor function 5 | let pairs: [(Key, fn(&KeyModifierState) -> bool, fn(&mut KeyModifierState) -> &mut bool); 8] = [ 6 | (KEY_LEFTCTRL.into(), |s| s.left_ctrl, |s: &mut KeyModifierState| &mut s.left_ctrl), 7 | (KEY_RIGHTCTRL.into(), |s| s.right_ctrl, |s: &mut KeyModifierState| &mut s.right_ctrl), 8 | (KEY_LEFTALT.into(), |s| s.left_alt, |s: &mut KeyModifierState| &mut s.left_alt), 9 | (KEY_RIGHTALT.into(), |s| s.right_alt, |s: &mut KeyModifierState| &mut s.right_alt), 10 | (KEY_LEFTSHIFT.into(), |s| s.left_shift, |s: &mut KeyModifierState| &mut s.left_shift), 11 | (KEY_RIGHTSHIFT.into(), |s| s.right_shift, |s: &mut KeyModifierState| &mut s.right_shift), 12 | (KEY_LEFTMETA.into(), |s| s.left_meta, |s: &mut KeyModifierState| &mut s.left_meta), 13 | (KEY_RIGHTMETA.into(), |s| s.right_meta, |s: &mut KeyModifierState| &mut s.right_meta), 14 | ]; 15 | 16 | for (key, is_modifier_down, modifier_mut) in pairs.iter() { 17 | if action.key.event_code == key.event_code && action.value == TYPE_DOWN && !is_modifier_down(&*modifiers) { 18 | let mut new_modifiers = modifiers.deref().deref().deref().clone(); 19 | *modifier_mut(&mut new_modifiers) = true; 20 | *modifiers = Arc::new(new_modifiers); 21 | return true; 22 | } else if action.key.event_code == key.event_code && action.value == TYPE_UP { 23 | let mut new_modifiers = modifiers.deref().deref().deref().clone(); 24 | *modifier_mut(&mut new_modifiers) = false; 25 | *modifiers = Arc::new(new_modifiers); 26 | return true; 27 | // TODO re-implement eating or throw it out completely 28 | // if ignore_list.is_ignored(&KeyAction::new(*key, TYPE_UP)) { 29 | // ignore_list.unignore(&KeyAction::new(*key, TYPE_UP)); 30 | // return; 31 | // } 32 | } else if action.value == TYPE_REPEAT { 33 | return true; 34 | } 35 | } 36 | false 37 | } 38 | -------------------------------------------------------------------------------- /src/event_loop.rs: -------------------------------------------------------------------------------- 1 | use std::thread; 2 | 3 | use pyo3::types::PyTuple; 4 | use pyo3::{IntoPy, Py, PyAny, Python}; 5 | 6 | use crate::*; 7 | 8 | #[derive(Debug)] 9 | pub enum PythonArgument { 10 | String(String), 11 | Number(i32), 12 | } 13 | 14 | type Args = Vec; 15 | 16 | pub fn args_to_py(py: Python<'_>, args: Args) -> &PyTuple { 17 | PyTuple::new( 18 | py, 19 | args.into_iter().map(|x| match x { 20 | PythonArgument::String(x) => x.into_py(py), 21 | PythonArgument::Number(x) => x.into_py(py), 22 | }), 23 | ) 24 | } 25 | 26 | pub struct EventLoop { 27 | thread_handle: Option>, 28 | callback_tx: tokio::sync::mpsc::Sender<(Py, Option)>, 29 | } 30 | 31 | impl EventLoop { 32 | pub fn new() -> Self { 33 | // TODO add exit channel 34 | let (callback_tx, mut callback_rx) = tokio::sync::mpsc::channel(128); 35 | let thread_handle = thread::spawn(move || { 36 | pyo3_asyncio::tokio::get_runtime().block_on(async move { 37 | // use std::time::Instant; 38 | // let now = Instant::now(); 39 | Python::with_gil(|py| { 40 | pyo3_asyncio::tokio::run::<_, ()>(py, async move { 41 | loop { 42 | let (callback_object, args): (Py, Option) = 43 | callback_rx.recv().await.expect("python runtime error: event loop channel is closed"); 44 | 45 | Python::with_gil(|py| { 46 | let args = args_to_py(py, args.unwrap_or_default()); 47 | 48 | let asyncio = py 49 | .import("asyncio") 50 | .expect("python runtime error: failed to import 'asyncio', is it installed?"); 51 | 52 | let is_async_callback: bool = asyncio 53 | .call_method1("iscoroutinefunction", (callback_object.as_ref(py),)) 54 | .expect("python runtime error: 'iscoroutinefunction' lookup failed") 55 | .extract() 56 | .expect("python runtime error: 'iscoroutinefunction' call failed"); 57 | 58 | if is_async_callback { 59 | let coroutine = callback_object 60 | .call(py, args, None) 61 | .expect("python runtime error: failed to call async callback"); 62 | 63 | let event_loop = pyo3_asyncio::tokio::get_current_loop(py) 64 | .expect("python runtime error: failed to get the event loop"); 65 | let coroutine = event_loop 66 | .call_method1("create_task", (coroutine,)) 67 | .expect("python runtime error: failed to create task"); 68 | 69 | // tasks only actually get run if we convert the coroutine to a rust future, even though we don't use it... 70 | if let Err(err) = pyo3_asyncio::tokio::into_future(coroutine) { 71 | eprintln!("an uncaught error was thrown by the python callback: {}", err); 72 | std::process::exit(1); 73 | } 74 | } else { 75 | if let Err(err) = callback_object.call(py, args, None) { 76 | eprintln!("an uncaught error was thrown by the python callback: {}", err); 77 | std::process::exit(1); 78 | } 79 | } 80 | }); 81 | } 82 | }) 83 | .expect("python runtime error: failed to start the event loop"); 84 | }); 85 | // let elapsed = now.elapsed(); 86 | }); 87 | }); 88 | 89 | EventLoop { thread_handle: Some(thread_handle), callback_tx } 90 | } 91 | pub fn execute(&self, callback_object: Py, args: Option) { 92 | self.callback_tx.try_send((callback_object, args)).expect(&ApplicationError::TooManyEvents.to_string()); 93 | } 94 | } 95 | 96 | lazy_static! { 97 | pub static ref EVENT_LOOP: Mutex = Mutex::new(EventLoop::new()); 98 | } 99 | -------------------------------------------------------------------------------- /src/global.rs: -------------------------------------------------------------------------------- 1 | use crate::xkb_transformer_registry::TransformerParams; 2 | use crate::*; 3 | 4 | lazy_static! { 5 | pub static ref DEFAULT_TRANSFORMER_PARAMS: RwLock = RwLock::new(TransformerParams::default()); 6 | } 7 | 8 | #[cfg(feature = "integration")] 9 | lazy_static! { 10 | pub static ref TEST_PIPE: Mutex> = Mutex::new(vec![]); 11 | } 12 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(fn_traits)] 2 | #![feature(type_alias_impl_trait)] 3 | #![recursion_limit = "256"] 4 | #![allow(warnings)] 5 | 6 | extern crate core; 7 | #[macro_use] 8 | extern crate lazy_static; 9 | extern crate regex; 10 | 11 | use arc_swap::ArcSwap; 12 | use arc_swap::ArcSwapOption; 13 | use std::borrow::BorrowMut; 14 | use std::hash::{DefaultHasher, Hash, Hasher}; 15 | use std::ops::{Deref, DerefMut}; 16 | use std::sync::{mpsc, Arc, Mutex, RwLock, Weak}; 17 | use std::thread; 18 | use std::time::Duration; 19 | use std::{fs, io}; 20 | 21 | pub use evdev_rs::enums::EV_ABS::*; 22 | pub use evdev_rs::enums::EV_KEY::*; 23 | pub use evdev_rs::enums::EV_REL::*; 24 | pub use key_primitives::Key; 25 | pub use parsing::*; 26 | 27 | pub use anyhow::{anyhow, Result}; 28 | use evdev_rs::enums::EventCode; 29 | pub use evdev_rs::InputEvent as EvdevInputEvent; 30 | use nom::lib::std::collections::{BTreeSet, HashMap, HashSet}; 31 | use tap::Tap; 32 | use uuid::Uuid; 33 | 34 | use event_loop::EVENT_LOOP; 35 | pub use mapper::*; 36 | pub use python::err_to_py; 37 | use reader::Reader; 38 | pub use subscriber::*; 39 | use writer::Writer; 40 | 41 | pub use crate::closure_channel::*; 42 | use crate::device::virtual_input_device::grab_udev_inputs; 43 | use crate::error::*; 44 | use crate::event::InputEvent; 45 | pub use crate::key_defs::*; 46 | use crate::key_primitives::*; 47 | 48 | pub mod capabilities; 49 | pub mod closure_channel; 50 | pub mod device; 51 | pub mod encoding; 52 | pub mod error; 53 | pub mod event; 54 | pub mod event_handlers; 55 | pub mod event_loop; 56 | pub mod global; 57 | pub mod key_defs; 58 | pub mod key_primitives; 59 | pub mod logging; 60 | pub mod parsing; 61 | pub mod platform; 62 | pub mod subscriber; 63 | pub mod xkb; 64 | pub mod xkb_transformer_registry; 65 | 66 | #[cfg(feature = "integration")] 67 | pub mod testing; 68 | 69 | pub mod mapper; 70 | pub mod python; 71 | pub mod reader; 72 | pub mod virtual_writer; 73 | pub mod window; 74 | pub mod writer; 75 | -------------------------------------------------------------------------------- /src/logging.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | pub fn print_debug(msg: impl AsRef) { 4 | println!("[DEBUG] {}", msg.as_ref()); 5 | } 6 | 7 | pub fn print_input_event(ev: &EvdevInputEvent) -> String { 8 | match ev.event_code { 9 | EventCode::EV_SYN(_) => ev.event_type().unwrap().to_string(), 10 | _ => format!( 11 | "Event: type: {}, code: {}, value: {}", 12 | ev.event_type().map(|ev_type| format!("{}", ev_type)).unwrap_or("None".to_string()), 13 | ev.event_code, 14 | ev.value, 15 | ), 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/man/man.rs: -------------------------------------------------------------------------------- 1 | use man::prelude::*; 2 | 3 | fn main() { 4 | let page = Manual::new("Map2") 5 | .about("A scripting language that allows complex key remapping on Linux.") 6 | .author(Author::new("shiro").email("shiro@usagi.io")) 7 | .flag(Flag::new() 8 | .help("Sets the verbosity level") 9 | .short("-v") 10 | .long("--verbose") 11 | ) 12 | .flag(Flag::new() 13 | .help("Selects the input devices") 14 | .short("-d") 15 | .long("--devices") 16 | ) 17 | .example(Example::new() 18 | .text("run a script") 19 | .command("map2 example.m2") 20 | .output("Runs the specified script.") 21 | ) 22 | .example(Example::new() 23 | .text("run a script and capture devices matched by the device list") 24 | .command("map2 -d device.list example.m2") 25 | .output("Captures devices that match the selectors in `device.list` and runs the script.") 26 | ) 27 | .example(Example::new() 28 | .text("run a script with maximum debug output") 29 | .command("map2 -vvv example.m2") 30 | .output("Runs the script example.m2 and outputs all debug information.") 31 | ) 32 | .custom( 33 | Section::new("devices") 34 | .paragraph(&["In order to capture device input it is necessary to configure which devices should get captured.", 35 | "A list of devices can be specified by providing a device list argument or by defining a default configuration", 36 | "in the user's configuration directory ($XDG_CONFIG_HOME/map2/device.list)."].join(" "))) 37 | .custom( 38 | Section::new("license") 39 | .paragraph("MIT") 40 | ) 41 | .render(); 42 | 43 | println!("{}", page); 44 | } 45 | -------------------------------------------------------------------------------- /src/mapper/mapper_util.rs: -------------------------------------------------------------------------------- 1 | use crate::python::*; 2 | use crate::*; 3 | 4 | use self::xkb::XKBTransformer; 5 | use crate::event_loop::{args_to_py, PythonArgument}; 6 | 7 | pub fn hash_path(path: &Vec) -> u64 { 8 | use std::hash::Hash; 9 | use std::hash::Hasher; 10 | let mut h = std::hash::DefaultHasher::new(); 11 | path.hash(&mut h); 12 | let path_hash = h.finish(); 13 | path_hash 14 | } 15 | 16 | #[derive(Debug, Clone)] 17 | pub enum PythonReturn { 18 | String(String), 19 | Bool(bool), 20 | } 21 | 22 | pub trait LinkDstState { 23 | // fn get_next(&self) -> Vec>; 24 | // fn get_next(&self) -> Box>>; 25 | // fn get_next(&self) -> std::iter::; 26 | } 27 | 28 | pub async fn run_python_handler( 29 | handler: Arc, 30 | args: Option>, 31 | ev: EvdevInputEvent, 32 | transformer: Arc, 33 | // next: &HashMap>, 34 | next: Vec>, 35 | ) -> Result<()> { 36 | println!("--> 1"); 37 | tokio::task::spawn_blocking(move || { 38 | let ret = Python::with_gil(|py| -> Result<()> { 39 | let asyncio = 40 | py.import("asyncio").expect("python runtime error: failed to import 'asyncio', is it installed?"); 41 | 42 | let is_async_callback: bool = asyncio 43 | .call_method1("iscoroutinefunction", (handler.deref().as_ref(py),)) 44 | .expect("python runtime error: 'iscoroutinefunction' lookup failed") 45 | .extract() 46 | .expect("python runtime error: 'iscoroutinefunction' call failed"); 47 | 48 | if is_async_callback { 49 | // TODO spawn a task here, run cb 50 | // EVENT_LOOP.lock().unwrap().execute(&handler, args); 51 | Ok(()) 52 | } else { 53 | let args = args_to_py(py, args.unwrap_or(vec![])); 54 | let ret = handler.call(py, args, None).map_err(|err| anyhow!("{}", err)).and_then(|ret| { 55 | if ret.is_none(py) { 56 | return Ok(None); 57 | } 58 | 59 | if let Ok(ret) = ret.extract::(py) { 60 | return Ok(Some(PythonReturn::String(ret))); 61 | } 62 | if let Ok(ret) = ret.extract::(py) { 63 | return Ok(Some(PythonReturn::Bool(ret))); 64 | } 65 | 66 | Err(anyhow!("unsupported python return value")) 67 | })?; 68 | 69 | match ret { 70 | Some(PythonReturn::String(ret)) => { 71 | let seq = parse_key_sequence(&ret, Some(&transformer))?; 72 | 73 | for action in seq.to_key_actions() { 74 | next.send_all(InputEvent::Raw(action.to_input_ev())); 75 | } 76 | } 77 | Some(PythonReturn::Bool(ret)) if ret => { 78 | next.send_all(InputEvent::Raw(ev.clone())); 79 | } 80 | _ => {} 81 | }; 82 | println!("--> 2"); 83 | Ok(()) 84 | } 85 | }); 86 | if let Err(err) = ret { 87 | eprintln!("{err}"); 88 | std::process::exit(1); 89 | } 90 | }) 91 | .await?; 92 | 93 | Ok(()) 94 | } 95 | -------------------------------------------------------------------------------- /src/mapper/mod.rs: -------------------------------------------------------------------------------- 1 | mod chord_mapper; 2 | mod mapper; 3 | mod mapper_util; 4 | mod mapping_functions; 5 | mod suffix_tree; 6 | mod text_mapper; 7 | 8 | pub use chord_mapper::ChordMapper; 9 | pub use mapper::{KeyMapperSnapshot, Mapper, MapperLink}; 10 | pub use mapping_functions::*; 11 | pub use text_mapper::TextMapper; 12 | 13 | use crate::subscriber::*; 14 | use mapper_util::*; 15 | -------------------------------------------------------------------------------- /src/mapper/suffix_tree.rs: -------------------------------------------------------------------------------- 1 | use nom::Slice; 2 | 3 | use crate::*; 4 | 5 | pub struct SuffixTree { 6 | root: HashMap>, 7 | } 8 | 9 | impl Default for SuffixTree { 10 | fn default() -> Self { 11 | Self { root: HashMap::new() } 12 | } 13 | } 14 | 15 | impl SuffixTree { 16 | pub fn new() -> Self { 17 | Self { root: HashMap::new() } 18 | } 19 | 20 | pub fn insert(&mut self, key: String, value: Value) { 21 | self.root.entry(key.chars().last().unwrap()).or_default().insert(&key[0..key.len() - 1], value); 22 | } 23 | 24 | pub fn get(&self, key: &String) -> Option<&Value> { 25 | self.root.get(&key.chars().last().unwrap()).and_then(|x| x.get(&key[0..key.len() - 1])) 26 | } 27 | } 28 | 29 | impl Clone for SuffixTree { 30 | fn clone(&self) -> Self { 31 | Self { root: self.root.clone() } 32 | } 33 | } 34 | 35 | pub struct SuffixTreeNode { 36 | value: Option, 37 | children: HashMap>, 38 | } 39 | 40 | impl Default for SuffixTreeNode { 41 | fn default() -> Self { 42 | Self { value: None, children: HashMap::new() } 43 | } 44 | } 45 | 46 | impl SuffixTreeNode { 47 | pub fn new() -> Self { 48 | Self { value: None, children: HashMap::new() } 49 | } 50 | 51 | pub fn insert(&mut self, key: &str, value: Value) { 52 | if let Some(ch) = key.chars().last() { 53 | self.children.entry(ch).or_default().insert(key.slice(0..key.len() - 1), value); 54 | } else { 55 | self.value = Some(value); 56 | } 57 | } 58 | 59 | pub fn get(&self, key: &str) -> Option<&Value> { 60 | if let Some(ch) = key.chars().last() { 61 | self.children.get(&ch).and_then(|x| x.get(key.slice(0..key.len() - 1))) 62 | } else { 63 | self.value.as_ref() 64 | } 65 | } 66 | } 67 | 68 | impl Clone for SuffixTreeNode { 69 | fn clone(&self) -> Self { 70 | Self { value: self.value.clone(), children: self.children.clone() } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/parsing/action_state.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | pub fn action_state(input: &str) -> ParseResult<&str, i32> { 4 | map(alt((tag_custom_no_case("down"), tag_custom_no_case("up"), tag_custom_no_case("repeat"))), |input: &str| { 5 | match &*input.to_lowercase() { 6 | "up" => 0, 7 | "down" => 1, 8 | "repeat" => 2, 9 | _ => unreachable!(), 10 | } 11 | })(input) 12 | } 13 | 14 | #[cfg(test)] 15 | mod tests { 16 | use super::*; 17 | 18 | #[test] 19 | fn action_state_input() { 20 | assert_eq!(action_state("up"), nom_ok(0)); 21 | assert_eq!(action_state("down"), nom_ok(1)); 22 | assert_eq!(action_state("repeat"), nom_ok(2)); 23 | } 24 | 25 | #[test] 26 | fn action_state_case() { 27 | assert_eq!(action_state("UP"), nom_ok(0)); 28 | assert_eq!(action_state("DOWN"), nom_ok(1)); 29 | assert_eq!(action_state("REPEAT"), nom_ok(2)); 30 | 31 | assert_eq!(action_state("DoWn"), nom_ok(1)); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/parsing/custom_combinators.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use nom::sequence::Tuple; 4 | use nom::IResult; 5 | use nom::{Compare, CompareResult, Err, InputLength, InputTake, Parser}; 6 | 7 | use crate::parsing::error::FromTagError; 8 | 9 | use super::*; 10 | 11 | pub fn many1_with_last_err(mut f: F) -> impl FnMut(I) -> IResult, E), E> 12 | where 13 | I: Clone + InputLength, 14 | F: Parser, 15 | E: nom::error::ParseError, 16 | { 17 | move |mut i: I| match f.parse(i.clone()) { 18 | Err(Err::Error(err)) => Err(Err::Error(E::append(i, nom::error::ErrorKind::Many1, err))), 19 | Err(e) => Err(e), 20 | Ok((i1, o)) => { 21 | let mut acc = Vec::with_capacity(4); 22 | acc.push(o); 23 | i = i1; 24 | 25 | loop { 26 | let len = i.input_len(); 27 | match f.parse(i.clone()) { 28 | Err(Err::Error(err)) => return Ok((i, (acc, err))), 29 | Err(e) => return Err(e), 30 | Ok((i1, o)) => { 31 | // infinite loop check: the parser must always consume 32 | if i1.input_len() == len { 33 | return Err(Err::Error(E::from_error_kind(i, nom::error::ErrorKind::Many1))); 34 | } 35 | 36 | i = i1; 37 | acc.push(o); 38 | } 39 | } 40 | } 41 | } 42 | } 43 | } 44 | 45 | pub fn tuple>>(mut l: List) -> impl FnMut(I) -> ParseResult { 46 | move |i: I| { 47 | let res = l.parse(i.clone()); 48 | if res.is_err() { 49 | return Err(make_generic_nom_err_new(i)); 50 | } 51 | res 52 | } 53 | } 54 | 55 | pub fn tag_custom>(tag: T) -> impl Fn(Input) -> IResult 56 | where 57 | Input: InputTake + Compare, 58 | T: InputLength + Clone + Display, 59 | { 60 | // let tag = tag.to_string(); 61 | move |input: Input| { 62 | let tag_len = tag.input_len(); 63 | let t = tag.clone(); 64 | 65 | let res: IResult<_, _, Error> = match input.compare(t) { 66 | CompareResult::Ok => Ok(input.take_split(tag_len)), 67 | _ => Err(Err::Error(Error::from_tag(input, tag.to_string()))), 68 | }; 69 | res 70 | } 71 | } 72 | 73 | pub fn tag_custom_no_case>( 74 | tag: T, 75 | ) -> impl Fn(Input) -> IResult 76 | where 77 | Input: InputTake + Compare, 78 | T: InputLength + Clone + Display, 79 | { 80 | // let tag = tag.to_string(); 81 | move |input: Input| { 82 | let tag_len = tag.input_len(); 83 | let t = tag.clone(); 84 | 85 | let res: IResult<_, _, Error> = match input.compare_no_case(t) { 86 | CompareResult::Ok => Ok(input.take_split(tag_len)), 87 | _ => Err(Err::Error(Error::from_tag(input, tag.to_string()))), 88 | }; 89 | res 90 | } 91 | } 92 | 93 | pub fn surrounded_group<'a, Output>( 94 | from_token: &'a str, 95 | to_token: &'a str, 96 | mut parser: impl FnMut(&'a str) -> ParseResult<&'a str, Output> + 'a, 97 | ) -> Box ParseResult<&'a str, Output> + 'a> { 98 | Box::new(move |input| { 99 | map_res( 100 | tuple((tag_custom(from_token), terminated(take_until(to_token), tag_custom(to_token)))), 101 | |(_, input)| { 102 | let (input, res) = parser(input)?; 103 | if !input.is_empty() { 104 | return Err(make_generic_nom_err_new(input)); 105 | } 106 | Ok(res) 107 | }, 108 | )(input) 109 | }) 110 | } 111 | -------------------------------------------------------------------------------- /src/parsing/error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | 3 | use nom::error::{ErrorKind, FromExternalError, ParseError}; 4 | use nom::InputLength; 5 | 6 | use super::*; 7 | 8 | pub type ParseResult = IResult>; 9 | 10 | pub fn make_generic_nom_err_new(input: I) -> NomErr> { 11 | NomErr::Error(CustomError { input, expected: vec![] }) 12 | } 13 | 14 | pub fn make_generic_nom_err_options(input: I, options: Vec) -> NomErr> { 15 | NomErr::Error(CustomError { input, expected: options }) 16 | } 17 | 18 | #[derive(Debug, PartialEq)] 19 | pub struct CustomError { 20 | pub input: I, 21 | pub expected: Vec, 22 | } 23 | 24 | impl ParseError for CustomError 25 | where 26 | I: InputLength, 27 | { 28 | fn from_error_kind(input: I, _: ErrorKind) -> Self { 29 | CustomError { input, expected: vec![] } 30 | } 31 | 32 | fn from_char(input: I, ch: char) -> Self { 33 | CustomError { input, expected: vec![ch.to_string()] } 34 | } 35 | 36 | fn or(mut self, mut other: Self) -> Self { 37 | if other.input.input_len() < self.input.input_len() { 38 | return other; 39 | } else if other.input.input_len() > self.input.input_len() { 40 | return self; 41 | } 42 | other.expected.append(&mut self.expected); 43 | other 44 | } 45 | 46 | fn append(_: I, _: ErrorKind, other: Self) -> Self { 47 | other 48 | } 49 | } 50 | 51 | impl FromExternalError for CustomError { 52 | fn from_external_error(input: I, _: ErrorKind, _: E) -> Self { 53 | Self { input, expected: vec![] } 54 | } 55 | } 56 | 57 | pub trait FromTagError: Sized { 58 | fn from_tag(input: I, tag: String) -> Self; 59 | } 60 | 61 | impl FromTagError for CustomError { 62 | fn from_tag(input: Input, tag: String) -> Self { 63 | Self { input, expected: vec![format!("'{}'", tag)] } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/parsing/identifier.rs: -------------------------------------------------------------------------------- 1 | use unicode_xid::UnicodeXID; 2 | 3 | use super::*; 4 | 5 | pub fn ident(input: &str) -> ParseResult<&str, String> { 6 | word(input).map_err(|err| make_generic_nom_err_options(input, vec!["identifier".to_string()])) 7 | } 8 | 9 | pub(super) fn word(input: &str) -> ParseResult<&str, String> { 10 | let (input, _) = multispace0(input)?; 11 | 12 | let mut chars = input.char_indices(); 13 | match chars.next() { 14 | Some((_, ch)) if UnicodeXID::is_xid_start(ch) || ch == '_' => {} 15 | _ => return Err(make_generic_nom_err_options(input, vec!["word".to_string()])), 16 | } 17 | 18 | while let Some((i, ch)) = chars.next() { 19 | if !UnicodeXID::is_xid_continue(ch) { 20 | return Ok((&input[i..], input[..i].into())); 21 | } 22 | } 23 | 24 | Ok((&input[input.len()..], input.into())) 25 | } 26 | 27 | #[cfg(test)] 28 | mod tests { 29 | use super::*; 30 | 31 | #[test] 32 | fn test_weird_names() { 33 | assert_eq!(ident("_foobar"), nom_ok("_foobar".to_string())); 34 | assert_eq!(ident("btn_forward"), nom_ok("btn_forward".to_string())); 35 | assert_eq!(ident("š"), nom_ok("š".to_string())); 36 | assert_eq!(ident("foo.bar"), nom_ok_rest(".bar", "foo".to_string())); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/parsing/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::*; 2 | use nom::branch::*; 3 | use nom::bytes::complete::*; 4 | use nom::character::complete::*; 5 | use nom::combinator::{map, map_res, opt, recognize}; 6 | use nom::multi::{many0, many1}; 7 | use nom::sequence::terminated; 8 | use nom::Err as NomErr; 9 | use nom::IResult; 10 | use tap::Tap; 11 | 12 | use custom_combinators::*; 13 | use error::*; 14 | use identifier::*; 15 | use key::*; 16 | use key_action::*; 17 | pub use key_action::{ParsedKeyAction, ParsedKeyActionVecExt}; 18 | use key_sequence::*; 19 | use motion_action::*; 20 | pub use public_parsing_api::*; 21 | 22 | use crate::*; 23 | 24 | mod action_state; 25 | mod custom_combinators; 26 | mod error; 27 | mod identifier; 28 | mod key; 29 | mod key_action; 30 | mod key_sequence; 31 | mod motion_action; 32 | mod public_parsing_api; 33 | 34 | #[cfg(test)] 35 | pub(super) fn nom_ok<'a, T>(value: T) -> ParseResult<&'a str, T> { 36 | Ok(("", value)) 37 | } 38 | 39 | #[cfg(test)] 40 | pub(super) fn nom_err(rest: I, expected: Vec) -> ParseResult { 41 | Err(NomErr::Error(CustomError { input: rest, expected })) 42 | } 43 | 44 | #[cfg(test)] 45 | pub(super) fn assert_nom_err(parse_result: ParseResult<&str, T>, rest: &str) { 46 | match parse_result { 47 | Err(NomErr::Error(x)) => { 48 | assert_eq!(x.input, rest); 49 | } 50 | Err(err) => { 51 | panic!("got other nom error: {}", err) 52 | } 53 | Ok((rest, res)) => { 54 | panic!("expected nom error, but got Ok\nresult: {:?}\nrest: '{}'\n", res, rest) 55 | } 56 | } 57 | } 58 | 59 | #[cfg(test)] 60 | pub(super) fn nom_ok_rest(rest: &str, value: T) -> ParseResult<&str, T> { 61 | Ok((rest, value)) 62 | } 63 | -------------------------------------------------------------------------------- /src/parsing/public_parsing_api.rs: -------------------------------------------------------------------------------- 1 | use crate::xkb::XKBTransformer; 2 | use evdev_rs::enums::EV_ABS; 3 | use itertools::Itertools; 4 | use nom::combinator::all_consuming; 5 | 6 | use super::*; 7 | 8 | fn format_err(err: NomErr>, input: &str, pos: usize) -> Error { 9 | match err { 10 | NomErr::Error(err) => { 11 | if err.expected.len() > 0 { 12 | anyhow!("{}\n{: >pos$}^ expected one of: {}", input, "", err.expected.iter().unique().join(", ")) 13 | } else { 14 | anyhow!("{}\n{: >pos$}^ failed here", input, "",) 15 | } 16 | } 17 | _ => anyhow!("failed to parse key mapping value"), 18 | } 19 | } 20 | 21 | pub fn parse_key_action_with_mods(raw: &str, transformer: Option<&XKBTransformer>) -> Result { 22 | let (rest, from) = single_key_action_utf_with_flags_utf(transformer)(raw).map_err(|err| format_err(err, raw, 0))?; 23 | 24 | if !rest.is_empty() { 25 | return Err(anyhow!("expected exactly 1 key action from input '{}'", raw)); 26 | } 27 | 28 | Ok(from) 29 | } 30 | 31 | pub fn parse_key_sequence(raw: &str, transformer: Option<&XKBTransformer>) -> Result> { 32 | let (rest, (res, last_err)) = key_sequence_utf(transformer)(raw).map_err(|err| format_err(err, raw, 0))?; 33 | 34 | if !rest.is_empty() { 35 | return Err(format_err(NomErr::Error(last_err), raw, raw.len() - rest.len())); 36 | } 37 | 38 | Ok(res) 39 | } 40 | 41 | pub fn parse_key(raw: &str, transformer: Option<&XKBTransformer>) -> Result { 42 | let (rest, ((key, flags))) = key_utf(transformer)(raw).map_err(|err| format_err(err, raw, 0))?; 43 | 44 | // if !rest.is_empty() { 45 | // return Err( 46 | // format_err(NomErr::Error(last_err), raw, raw.len() - rest.len()) 47 | // ); 48 | // } 49 | // TODO errr handling 50 | 51 | Ok(key) 52 | } 53 | 54 | pub fn parse_abs_tag(input: &str) -> Result { 55 | all_consuming(abs_tag)(input).map(|(_, x)| x).map_err(|_| anyhow!("invalid input")) 56 | } 57 | -------------------------------------------------------------------------------- /src/platform/mod.rs: -------------------------------------------------------------------------------- 1 | use std::process::Command; 2 | 3 | pub enum Platform { 4 | Hyprland, 5 | X11, 6 | Unknown, 7 | } 8 | 9 | pub fn get_platform() -> Platform { 10 | if platform_is_hyprland() { 11 | return Platform::Hyprland; 12 | } 13 | if platform_is_x11() { 14 | return Platform::X11; 15 | } 16 | Platform::Unknown 17 | } 18 | 19 | fn platform_is_hyprland() -> bool { 20 | Command::new("printenv") 21 | .arg("HYPRLAND_INSTANCE_SIGNATURE") 22 | .stdout(std::process::Stdio::null()) 23 | .stderr(std::process::Stdio::null()) 24 | .status() 25 | .map(|status| status.success()) 26 | .unwrap_or(false) 27 | } 28 | 29 | fn platform_is_x11() -> bool { 30 | Command::new("printenv") 31 | .arg("XDG_SESSION_TYPE") 32 | .output() 33 | .map(|info| info.status.success() && String::from_utf8_lossy(&info.stdout).replace("\n", "") == "x11") 34 | .unwrap_or(false) 35 | } 36 | 37 | // fn platform_is_sway() -> bool { 38 | // Command::new("printenv") 39 | // .arg("SWAYSOCK") 40 | // .status() 41 | // .map(|status| status.success()) 42 | // .unwrap_or(false) 43 | // } 44 | 45 | // for kde/kwin (wayland) 46 | // https://unix.stackexchange.com/questions/706477/is-there-a-way-to-get-list-of-windows-on-kde-wayland 47 | 48 | // for gnome 49 | // https://github.com/ActivityWatch/aw-watcher-window/pull/46/files 50 | -------------------------------------------------------------------------------- /src/python.rs: -------------------------------------------------------------------------------- 1 | pub use pyo3::exceptions::PyRuntimeError; 2 | pub use pyo3::impl_::wrap::OkWrap; 3 | pub use pyo3::prelude::*; 4 | pub use pyo3::types::PyDict; 5 | pub use pyo3::PyClass; 6 | use signal_hook::{consts::SIGINT, iterator::Signals}; 7 | use tokio::runtime::Runtime; 8 | 9 | use crate::virtual_writer::VirtualWriter; 10 | use crate::window::Window; 11 | use crate::*; 12 | 13 | #[pyclass] 14 | struct PyKey { 15 | #[pyo3(get, set)] 16 | code: u32, 17 | #[pyo3(get, set)] 18 | value: i32, 19 | } 20 | 21 | #[pyfunction] 22 | #[pyo3(signature = (* * options))] 23 | fn default(options: Option<&PyDict>) -> PyResult<()> { 24 | let options: HashMap<&str, &PyAny> = match options { 25 | Some(py_dict) => py_dict.extract().unwrap(), 26 | None => HashMap::new(), 27 | }; 28 | 29 | let kbd_model: Option = options.get("model").and_then(|x| x.extract().ok()); 30 | let kbd_layout: Option = options.get("layout").and_then(|x| x.extract().ok()); 31 | let kbd_variant: Option> = options.get("variant").and_then(|x| x.extract().ok()); 32 | let kbd_options: Option> = options.get("options").and_then(|x| x.extract().ok()); 33 | 34 | if kbd_model.is_some() || kbd_layout.is_some() || kbd_variant.is_some() || kbd_options.is_some() { 35 | let mut default_params = global::DEFAULT_TRANSFORMER_PARAMS.write().unwrap(); 36 | 37 | if let Some(model) = kbd_model { 38 | default_params.model = model; 39 | } 40 | if let Some(layout) = kbd_layout { 41 | default_params.layout = layout; 42 | } 43 | if let Some(variant) = kbd_variant { 44 | default_params.variant = variant; 45 | } 46 | if let Some(options) = kbd_options { 47 | default_params.options = options; 48 | } 49 | } 50 | Ok(()) 51 | } 52 | 53 | #[pyfunction] 54 | fn link(py: Python, mut chain: Vec) -> PyResult<()> { 55 | let mut prev: Option> = None; 56 | 57 | if chain.len() < 2 { 58 | return Err(PyRuntimeError::new_err("expected at least 2 nodes")); 59 | } 60 | 61 | let chain_len = chain.len(); 62 | 63 | let last = node_to_link_dst(chain.remove(chain_len - 1).as_ref(py)).ok_or_else(|| { 64 | PyRuntimeError::new_err(format!("expected node at index {} to be a source node", chain_len - 1)) 65 | })?; 66 | 67 | let mut prev = node_to_link_src(chain.remove(0).as_ref(py)) 68 | .ok_or_else(|| PyRuntimeError::new_err("expected node at index 0 to be a source node"))?; 69 | 70 | let chain = chain 71 | .into_iter() 72 | .enumerate() 73 | .map(|(idx, node)| { 74 | Ok(( 75 | node_to_link_src(node.as_ref(py)).ok_or_else(|| { 76 | PyRuntimeError::new_err(format!( 77 | "expected node at index {} to be a source/desination node", 78 | idx + 1 79 | )) 80 | })?, 81 | node_to_link_dst(node.as_ref(py)).ok_or_else(|| { 82 | PyRuntimeError::new_err(format!( 83 | "expected node at index {} to be a source/desination node", 84 | idx + 1 85 | )) 86 | })?, 87 | )) 88 | }) 89 | .collect::, PyErr>>()?; 90 | 91 | chain.into_iter().for_each(|node| { 92 | prev.link_to(node.1.clone()); 93 | node.1.link_from(prev.clone()); 94 | prev = node.0; 95 | }); 96 | 97 | prev.link_to(last.clone()); 98 | last.link_from(prev); 99 | 100 | Ok(()) 101 | } 102 | 103 | pub fn err_to_py(err: anyhow::Error) -> PyErr { 104 | PyRuntimeError::new_err(err.to_string()) 105 | } 106 | 107 | pub fn get_runtime<'a>() -> &'a Runtime { 108 | pyo3_asyncio::tokio::get_runtime() 109 | } 110 | 111 | #[pyfunction] 112 | fn wait(py: Python) { 113 | #[cfg(not(feature = "integration"))] 114 | py.allow_threads(|| { 115 | let mut signals = Signals::new(&[SIGINT]).unwrap(); 116 | for _ in signals.forever() { 117 | std::process::exit(0); 118 | } 119 | }); 120 | } 121 | 122 | #[pyfunction] 123 | fn exit(exit_code: Option) { 124 | #[cfg(not(feature = "integration"))] 125 | std::process::exit(exit_code.unwrap_or(0)); 126 | } 127 | 128 | #[cfg(feature = "integration")] 129 | #[pyfunction] 130 | fn __test() -> PyResult> { 131 | Ok(global::TEST_PIPE.lock().unwrap().iter().map(|x| serde_json::to_string(x).unwrap()).collect()) 132 | } 133 | 134 | #[pymodule] 135 | fn map2(_py: Python, m: &PyModule) -> PyResult<()> { 136 | m.add_function(wrap_pyfunction!(wait, m)?)?; 137 | m.add_function(wrap_pyfunction!(exit, m)?)?; 138 | m.add_function(wrap_pyfunction!(default, m)?)?; 139 | m.add_function(wrap_pyfunction!(link, m)?)?; 140 | #[cfg(feature = "integration")] 141 | m.add_function(wrap_pyfunction!(__test, m)?)?; 142 | m.add_class::()?; 143 | m.add_class::()?; 144 | m.add_class::()?; 145 | m.add_class::()?; 146 | m.add_class::()?; 147 | m.add_class::()?; 148 | m.add_class::()?; 149 | m.add_class::()?; 150 | 151 | Ok(()) 152 | } 153 | -------------------------------------------------------------------------------- /src/subscriber.rs: -------------------------------------------------------------------------------- 1 | use crate::python::*; 2 | use pyo3::{PyAny, PyRefMut}; 3 | 4 | use crate::mapper::*; 5 | use crate::*; 6 | 7 | pub fn node_to_link_dst(target: &PyAny) -> Option> { 8 | if let Ok(target) = target.extract::>() { 9 | return Some(target.link.clone()); 10 | } 11 | if let Ok(mut target) = target.extract::>() { 12 | return Some(target.link.clone()); 13 | } 14 | if let Ok(target) = target.extract::>() { 15 | return Some(target.link.clone()); 16 | } 17 | if let Ok(target) = target.extract::>() { 18 | return Some(target.link.clone()); 19 | } 20 | None 21 | } 22 | 23 | pub fn node_to_link_src(target: &PyAny) -> Option> { 24 | if let Ok(target) = target.extract::>() { 25 | return Some(target.link.clone()); 26 | } 27 | if let Ok(target) = target.extract::>() { 28 | return Some(target.link.clone()); 29 | } 30 | if let Ok(mut target) = target.extract::>() { 31 | return Some(target.link.clone()); 32 | } 33 | if let Ok(target) = target.extract::>() { 34 | return Some(target.link.clone()); 35 | } 36 | None 37 | } 38 | 39 | pub trait LinkSrc: Send + Sync { 40 | fn id(&self) -> &Uuid; 41 | fn link_to(&self, node: Arc) -> Result<()>; 42 | fn unlink_to(&self, id: &Uuid) -> Result; 43 | } 44 | 45 | pub trait LinkDst: Send + Sync { 46 | fn id(&self) -> &Uuid; 47 | fn link_from(&self, node: Arc) -> Result<()>; 48 | fn unlink_from(&self, id: &Uuid) -> Result; 49 | fn send(&self, ev: InputEvent) -> Result<()>; 50 | } 51 | 52 | pub trait SubscriberHashmapExt { 53 | fn send_all(&self, ev: InputEvent); 54 | } 55 | 56 | impl SubscriberHashmapExt for HashMap> { 57 | fn send_all(&self, ev: InputEvent) { 58 | self.values().for_each(|link| { 59 | // TODO handle err 60 | link.send(ev.clone()); 61 | }); 62 | } 63 | } 64 | 65 | pub trait SubscriberVecExt { 66 | fn send_all(&self, ev: InputEvent); 67 | } 68 | 69 | impl SubscriberVecExt for Vec> { 70 | fn send_all(&self, ev: InputEvent) { 71 | self.iter().for_each(|link| { 72 | link.send(ev.clone()); 73 | }); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/testing/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | #[cfg(feature = "integration")] 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[cfg(feature = "integration")] 6 | #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] 7 | pub enum TestEvent { 8 | WriterOutEv(EvdevInputEvent), 9 | } 10 | -------------------------------------------------------------------------------- /src/window/hyprland_window.rs: -------------------------------------------------------------------------------- 1 | use crate::python::*; 2 | use crate::window::window_base::{ActiveWindowInfo, WindowControlMessage, WindowHandler}; 3 | use crate::*; 4 | use hyprland::async_closure; 5 | use hyprland::data::Client; 6 | use hyprland::data::{Monitor, Workspace}; 7 | use hyprland::event_listener::WindowEventData; 8 | use hyprland::event_listener::{AsyncEventListener, EventListener}; 9 | use hyprland::shared::HyprDataActive; 10 | use hyprland::shared::{HyprData, HyprDataActiveOptional}; 11 | use std::panic::catch_unwind; 12 | 13 | pub fn hyprland_window_handler() -> WindowHandler { 14 | Box::new( 15 | |exit_rx: oneshot::Receiver<()>, 16 | mut subscription_rx: tokio::sync::mpsc::Receiver| 17 | -> Result<()> { 18 | let subscriptions: Arc>>> = Arc::new(Mutex::new(HashMap::new())); 19 | 20 | let prev_hook = std::panic::take_hook(); 21 | std::panic::set_hook(Box::new(|_info| {})); 22 | 23 | let mut event_listener = catch_unwind(|| AsyncEventListener::new()).map_err(|err| { 24 | anyhow!( 25 | "hyprland connection error: {}", 26 | err.downcast::().unwrap_or(Box::new("unknown".to_string())) 27 | ) 28 | })?; 29 | 30 | std::panic::set_hook(prev_hook); 31 | 32 | let handle_window_change = { 33 | let subscriptions = subscriptions.clone(); 34 | move |info: ActiveWindowInfo| { 35 | tokio::task::spawn_blocking(move || { 36 | Python::with_gil(|py| { 37 | let subscriptions = { subscriptions.lock().unwrap().values().cloned().collect::>() }; 38 | for callback in subscriptions { 39 | let is_callable = callback.as_ref(py).is_callable(); 40 | if !is_callable { 41 | continue; 42 | } 43 | 44 | let ret = callback.call(py, (info.class.clone(),), None); 45 | 46 | if let Err(err) = ret { 47 | eprintln!("{err}"); 48 | std::process::exit(1); 49 | } 50 | } 51 | }); 52 | }); 53 | } 54 | }; 55 | 56 | event_listener.add_active_window_changed_handler(move |info| { 57 | Box::pin({ 58 | let handle_window_change = handle_window_change.clone(); 59 | async move { 60 | let info = info.unwrap(); 61 | handle_window_change(ActiveWindowInfo { 62 | class: info.class, 63 | instance: "".to_string(), 64 | name: info.title, 65 | }); 66 | } 67 | }) 68 | }); 69 | 70 | tokio::task::spawn(async move { 71 | event_listener.start_listener_async().await; 72 | }); 73 | 74 | tokio::task::spawn(async move { 75 | loop { 76 | let msg = match subscription_rx.recv().await { 77 | Some(v) => v, 78 | None => return, 79 | }; 80 | match msg { 81 | WindowControlMessage::Subscribe(id, callback) => { 82 | subscriptions.lock().unwrap().insert(id, callback.clone()); 83 | 84 | if let Ok(Some(info)) = Client::get_active_async().await { 85 | println!(" --> w1"); 86 | //if !is_callable { continue; } 87 | 88 | tokio::task::spawn_blocking(move || { 89 | Python::with_gil(|py| { 90 | println!(" --> w1 start"); 91 | let is_callable = callback.as_ref(py).is_callable(); 92 | let ret = callback.call(py, (info.class.clone(),), None); 93 | if let Err(err) = ret { 94 | eprintln!("{err}"); 95 | std::process::exit(1); 96 | } 97 | println!(" --> w1 done"); 98 | }); 99 | }); 100 | } 101 | } 102 | WindowControlMessage::Unsubscribe(id) => {} 103 | } 104 | } 105 | }); 106 | 107 | Ok(()) 108 | }, 109 | ) 110 | } 111 | -------------------------------------------------------------------------------- /src/window/mod.rs: -------------------------------------------------------------------------------- 1 | mod hyprland_window; 2 | mod window_base; 3 | mod x11_window; 4 | 5 | pub use window_base::Window; 6 | -------------------------------------------------------------------------------- /src/window/window_base.rs: -------------------------------------------------------------------------------- 1 | use crate::platform::{get_platform, Platform}; 2 | use crate::python::*; 3 | use crate::window::hyprland_window::hyprland_window_handler; 4 | use crate::window::x11_window::x11_window_handler; 5 | use crate::*; 6 | 7 | #[derive(Debug, Clone)] 8 | pub struct ActiveWindowInfo { 9 | pub class: String, 10 | pub instance: String, 11 | pub name: String, 12 | } 13 | 14 | pub type WindowHandler = 15 | Box, tokio::sync::mpsc::Receiver) -> Result<()> + Send + Sync>; 16 | 17 | #[pyclass] 18 | pub struct Window { 19 | thread_handle: Option>, 20 | thread_exit_tx: Option>, 21 | subscription_id_cnt: u32, 22 | subscription_tx: tokio::sync::mpsc::Sender, 23 | } 24 | 25 | #[pymethods] 26 | impl Window { 27 | #[new] 28 | pub fn new() -> Self { 29 | let handler = match get_platform() { 30 | Platform::Hyprland => hyprland_window_handler(), 31 | Platform::X11 => x11_window_handler(), 32 | Platform::Unknown => { 33 | eprintln!("{}", ApplicationError::UnsupportedPlatform); 34 | std::process::exit(1); 35 | } 36 | }; 37 | 38 | let (subscription_tx, thread_handle, thread_exit_tx) = spawn_listener_thread(handler); 39 | 40 | Window { 41 | thread_handle: Some(thread_handle), 42 | thread_exit_tx: Some(thread_exit_tx), 43 | subscription_id_cnt: 0, 44 | subscription_tx, 45 | } 46 | } 47 | 48 | fn on_window_change(&mut self, callback: PyObject) -> WindowOnWindowChangeSubscription { 49 | self.subscription_tx.try_send(WindowControlMessage::Subscribe(self.subscription_id_cnt, callback)).unwrap(); 50 | let subscription = WindowOnWindowChangeSubscription { id: self.subscription_id_cnt }; 51 | self.subscription_id_cnt += 1; 52 | subscription 53 | } 54 | fn remove_on_window_change(&self, subscription: &WindowOnWindowChangeSubscription) { 55 | let _ = self.subscription_tx.send(WindowControlMessage::Unsubscribe(subscription.id)); 56 | } 57 | } 58 | 59 | impl Drop for Window { 60 | fn drop(&mut self) { 61 | let _ = self.thread_exit_tx.take().unwrap().send(()); 62 | let _ = self.thread_handle.take().unwrap()/*.try_timed_join(Duration::from_millis(100)).unwrap()*/; 63 | } 64 | } 65 | 66 | #[pyclass] 67 | struct WindowOnWindowChangeSubscription { 68 | id: u32, 69 | } 70 | 71 | pub enum WindowControlMessage { 72 | Subscribe(u32, PyObject), 73 | Unsubscribe(u32), 74 | } 75 | 76 | pub fn spawn_listener_thread( 77 | handler: WindowHandler, 78 | ) -> (tokio::sync::mpsc::Sender, tokio::task::JoinHandle<()>, oneshot::Sender<()>) { 79 | let (subscription_tx, subscription_rx) = tokio::sync::mpsc::channel(255); 80 | let (exit_tx, exit_rx) = oneshot::channel(); 81 | let handle = get_runtime().spawn(async move { 82 | if let Err(err) = handler(exit_rx, subscription_rx) { 83 | eprintln!("{}", err); 84 | std::process::exit(1); 85 | } 86 | }); 87 | (subscription_tx, handle, exit_tx) 88 | } 89 | -------------------------------------------------------------------------------- /src/xkb_transformer_registry.rs: -------------------------------------------------------------------------------- 1 | use crate::xkb::XKBTransformer; 2 | use crate::*; 3 | use std::collections::HashMap; 4 | 5 | #[derive(Clone, Eq, PartialEq, Hash)] 6 | pub struct TransformerParams { 7 | pub model: String, 8 | pub layout: String, 9 | pub variant: Option, 10 | pub options: Option, 11 | } 12 | 13 | impl TransformerParams { 14 | pub fn new( 15 | model: Option, 16 | layout: Option, 17 | variant: Option, 18 | options: Option, 19 | ) -> Self { 20 | let default = global::DEFAULT_TRANSFORMER_PARAMS.read().unwrap(); 21 | let model = model.unwrap_or(default.model.clone()); 22 | let layout = layout.unwrap_or(default.layout.clone()); 23 | let variant = variant.or(default.variant.clone()); 24 | let options = options.or(default.options.clone()); 25 | Self { model, layout, variant, options } 26 | } 27 | } 28 | 29 | impl Default for TransformerParams { 30 | fn default() -> Self { 31 | Self { model: "pc105".to_string(), layout: "us".to_string(), variant: None, options: None } 32 | } 33 | } 34 | 35 | pub struct XKBTransformerRegistry { 36 | registry: Mutex>>, 37 | } 38 | 39 | impl XKBTransformerRegistry { 40 | pub fn new() -> Self { 41 | Self { registry: Mutex::new(HashMap::new()) } 42 | } 43 | 44 | pub fn get(&self, params: &TransformerParams) -> Result> { 45 | let mut registry = self.registry.lock().unwrap(); 46 | let res = registry.get(¶ms); 47 | 48 | match res { 49 | Some(f) => match f.upgrade() { 50 | Some(transformer) => Ok(transformer), 51 | None => { 52 | let transformer = Arc::new(XKBTransformer::new( 53 | ¶ms.model, 54 | ¶ms.layout, 55 | params.variant.as_deref(), 56 | params.options.clone(), 57 | )?); 58 | registry.insert(params.clone(), Arc::downgrade(&transformer)); 59 | Ok(transformer) 60 | } 61 | }, 62 | None => { 63 | let transformer = Arc::new(XKBTransformer::new( 64 | ¶ms.model, 65 | ¶ms.layout, 66 | params.variant.as_deref(), 67 | params.options.clone(), 68 | )?); 69 | registry.insert(params.clone(), Arc::downgrade(&transformer)); 70 | Ok(transformer) 71 | } 72 | } 73 | } 74 | } 75 | 76 | lazy_static! { 77 | pub static ref XKB_TRANSFORMER_REGISTRY: XKBTransformerRegistry = XKBTransformerRegistry::new(); 78 | } 79 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | cargo test --no-default-features --features integration $@ 6 | --------------------------------------------------------------------------------