ParseResult<&'a str, Output> + 'a> {
98 | Box::new(move |input| {
99 | map_res(
100 | tuple((tag_custom(from_token), terminated(take_until(to_token), tag_custom(to_token)))),
101 | |(_, input)| {
102 | let (input, res) = parser(input)?;
103 | if !input.is_empty() {
104 | return Err(make_generic_nom_err_new(input));
105 | }
106 | Ok(res)
107 | },
108 | )(input)
109 | })
110 | }
111 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/event_loop.rs:
--------------------------------------------------------------------------------
1 | use std::thread;
2 |
3 | use pyo3::types::PyTuple;
4 | use pyo3::{IntoPy, Py, PyAny, Python};
5 |
6 | use crate::*;
7 |
8 | #[derive(Debug)]
9 | pub enum PythonArgument {
10 | String(String),
11 | Number(i32),
12 | }
13 |
14 | type Args = Vec;
15 |
16 | pub fn args_to_py(py: Python<'_>, args: Args) -> &PyTuple {
17 | PyTuple::new(
18 | py,
19 | args.into_iter().map(|x| match x {
20 | PythonArgument::String(x) => x.into_py(py),
21 | PythonArgument::Number(x) => x.into_py(py),
22 | }),
23 | )
24 | }
25 |
26 | pub struct EventLoop {
27 | thread_handle: Option>,
28 | callback_tx: tokio::sync::mpsc::Sender<(Py, Option)>,
29 | }
30 |
31 | impl EventLoop {
32 | pub fn new() -> Self {
33 | // TODO add exit channel
34 | let (callback_tx, mut callback_rx) = tokio::sync::mpsc::channel(128);
35 | let thread_handle = thread::spawn(move || {
36 | pyo3_asyncio::tokio::get_runtime().block_on(async move {
37 | // use std::time::Instant;
38 | // let now = Instant::now();
39 | Python::with_gil(|py| {
40 | pyo3_asyncio::tokio::run::<_, ()>(py, async move {
41 | loop {
42 | let (callback_object, args): (Py, Option) =
43 | callback_rx.recv().await.expect("python runtime error: event loop channel is closed");
44 |
45 | Python::with_gil(|py| {
46 | let args = args_to_py(py, args.unwrap_or_default());
47 |
48 | let asyncio = py
49 | .import("asyncio")
50 | .expect("python runtime error: failed to import 'asyncio', is it installed?");
51 |
52 | let is_async_callback: bool = asyncio
53 | .call_method1("iscoroutinefunction", (callback_object.as_ref(py),))
54 | .expect("python runtime error: 'iscoroutinefunction' lookup failed")
55 | .extract()
56 | .expect("python runtime error: 'iscoroutinefunction' call failed");
57 |
58 | if is_async_callback {
59 | let coroutine = callback_object
60 | .call(py, args, None)
61 | .expect("python runtime error: failed to call async callback");
62 |
63 | let event_loop = pyo3_asyncio::tokio::get_current_loop(py)
64 | .expect("python runtime error: failed to get the event loop");
65 | let coroutine = event_loop
66 | .call_method1("create_task", (coroutine,))
67 | .expect("python runtime error: failed to create task");
68 |
69 | // tasks only actually get run if we convert the coroutine to a rust future, even though we don't use it...
70 | if let Err(err) = pyo3_asyncio::tokio::into_future(coroutine) {
71 | eprintln!("an uncaught error was thrown by the python callback: {}", err);
72 | std::process::exit(1);
73 | }
74 | } else {
75 | if let Err(err) = callback_object.call(py, args, None) {
76 | eprintln!("an uncaught error was thrown by the python callback: {}", err);
77 | std::process::exit(1);
78 | }
79 | }
80 | });
81 | }
82 | })
83 | .expect("python runtime error: failed to start the event loop");
84 | });
85 | // let elapsed = now.elapsed();
86 | });
87 | });
88 |
89 | EventLoop { thread_handle: Some(thread_handle), callback_tx }
90 | }
91 | pub fn execute(&self, callback_object: Py, args: Option) {
92 | self.callback_tx.try_send((callback_object, args)).expect(&ApplicationError::TooManyEvents.to_string());
93 | }
94 | }
95 |
96 | lazy_static! {
97 | pub static ref EVENT_LOOP: Mutex = Mutex::new(EventLoop::new());
98 | }
99 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/src/window/hyprland_window.rs:
--------------------------------------------------------------------------------
1 | use crate::python::*;
2 | use crate::window::window_base::{ActiveWindowInfo, WindowControlMessage, WindowHandler};
3 | use crate::*;
4 | use hyprland::async_closure;
5 | use hyprland::data::Client;
6 | use hyprland::data::{Monitor, Workspace};
7 | use hyprland::event_listener::WindowEventData;
8 | use hyprland::event_listener::{AsyncEventListener, EventListener};
9 | use hyprland::shared::HyprDataActive;
10 | use hyprland::shared::{HyprData, HyprDataActiveOptional};
11 | use std::panic::catch_unwind;
12 |
13 | pub fn hyprland_window_handler() -> WindowHandler {
14 | Box::new(
15 | |exit_rx: oneshot::Receiver<()>,
16 | mut subscription_rx: tokio::sync::mpsc::Receiver|
17 | -> Result<()> {
18 | let subscriptions: Arc>>> = Arc::new(Mutex::new(HashMap::new()));
19 |
20 | let prev_hook = std::panic::take_hook();
21 | std::panic::set_hook(Box::new(|_info| {}));
22 |
23 | let mut event_listener = catch_unwind(|| AsyncEventListener::new()).map_err(|err| {
24 | anyhow!(
25 | "hyprland connection error: {}",
26 | err.downcast::().unwrap_or(Box::new("unknown".to_string()))
27 | )
28 | })?;
29 |
30 | std::panic::set_hook(prev_hook);
31 |
32 | let handle_window_change = {
33 | let subscriptions = subscriptions.clone();
34 | move |info: ActiveWindowInfo| {
35 | tokio::task::spawn_blocking(move || {
36 | Python::with_gil(|py| {
37 | let subscriptions = { subscriptions.lock().unwrap().values().cloned().collect::>() };
38 | for callback in subscriptions {
39 | let is_callable = callback.as_ref(py).is_callable();
40 | if !is_callable {
41 | continue;
42 | }
43 |
44 | let ret = callback.call(py, (info.class.clone(),), None);
45 |
46 | if let Err(err) = ret {
47 | eprintln!("{err}");
48 | std::process::exit(1);
49 | }
50 | }
51 | });
52 | });
53 | }
54 | };
55 |
56 | event_listener.add_active_window_changed_handler(move |info| {
57 | Box::pin({
58 | let handle_window_change = handle_window_change.clone();
59 | async move {
60 | let info = info.unwrap();
61 | handle_window_change(ActiveWindowInfo {
62 | class: info.class,
63 | instance: "".to_string(),
64 | name: info.title,
65 | });
66 | }
67 | })
68 | });
69 |
70 | tokio::task::spawn(async move {
71 | event_listener.start_listener_async().await;
72 | });
73 |
74 | tokio::task::spawn(async move {
75 | loop {
76 | let msg = match subscription_rx.recv().await {
77 | Some(v) => v,
78 | None => return,
79 | };
80 | match msg {
81 | WindowControlMessage::Subscribe(id, callback) => {
82 | subscriptions.lock().unwrap().insert(id, callback.clone());
83 |
84 | if let Ok(Some(info)) = Client::get_active_async().await {
85 | println!(" --> w1");
86 | //if !is_callable { continue; }
87 |
88 | tokio::task::spawn_blocking(move || {
89 | Python::with_gil(|py| {
90 | println!(" --> w1 start");
91 | let is_callable = callback.as_ref(py).is_callable();
92 | let ret = callback.call(py, (info.class.clone(),), None);
93 | if let Err(err) = ret {
94 | eprintln!("{err}");
95 | std::process::exit(1);
96 | }
97 | println!(" --> w1 done");
98 | });
99 | });
100 | }
101 | }
102 | WindowControlMessage::Unsubscribe(id) => {}
103 | }
104 | }
105 | });
106 |
107 | Ok(())
108 | },
109 | )
110 | }
111 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/evdev-rs/evdev-sys/build.rs:
--------------------------------------------------------------------------------
1 | use std::env;
2 | use std::ffi::OsString;
3 | use std::fs;
4 | use std::path::{Path, PathBuf};
5 | use std::process::Command;
6 |
7 | #[cfg(feature = "libevdev-1-10")]
8 | // ver_str is string of the form "major.minor.patch"
9 | fn parse_version(ver_str: &str) -> 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 |
--------------------------------------------------------------------------------
/src/python.rs:
--------------------------------------------------------------------------------
1 | pub use pyo3::exceptions::PyRuntimeError;
2 | pub use pyo3::impl_::wrap::OkWrap;
3 | pub use pyo3::prelude::*;
4 | pub use pyo3::types::PyDict;
5 | pub use pyo3::PyClass;
6 | use signal_hook::{consts::SIGINT, iterator::Signals};
7 | use tokio::runtime::Runtime;
8 |
9 | use crate::virtual_writer::VirtualWriter;
10 | use crate::window::Window;
11 | use crate::*;
12 |
13 | #[pyclass]
14 | struct PyKey {
15 | #[pyo3(get, set)]
16 | code: u32,
17 | #[pyo3(get, set)]
18 | value: i32,
19 | }
20 |
21 | #[pyfunction]
22 | #[pyo3(signature = (* * options))]
23 | fn default(options: Option<&PyDict>) -> PyResult<()> {
24 | let options: HashMap<&str, &PyAny> = match options {
25 | Some(py_dict) => py_dict.extract().unwrap(),
26 | None => HashMap::new(),
27 | };
28 |
29 | let kbd_model: Option = options.get("model").and_then(|x| x.extract().ok());
30 | let kbd_layout: Option = options.get("layout").and_then(|x| x.extract().ok());
31 | let kbd_variant: Option