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 | [](https://github.com/shiro/map2)
6 | [](https://github.com/shiro/map2/blob/master/LICENSE)
7 | [](https://discord.gg/brKgH43XQN)
8 | [](https://github.com/shiro/map2/actions/workflows/CI.yml)
9 | [](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 |

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 | [](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 |
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 |
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 | Skip to Content
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 | ,
21 |
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 | Key names |
163 | Description |
164 |
165 |
166 | {(key) =>
167 |
168 |
169 |
170 | {aliases[key]}
171 |
172 |
173 | {key}
174 | |
175 | {descriptions[key]} |
176 |
177 | }
178 |
179 |
180 |
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 |
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 | [](https://travis-ci.org/ndesh26/evdev-rs)
4 | [](https://crates.io/crates/evdev-rs)
5 | [](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::