Skip to content

Commit

Permalink
Make window platform agnostic, add 'wasd mouse' example
Browse files Browse the repository at this point in the history
  • Loading branch information
shiro committed Nov 22, 2023
1 parent a0c22ab commit 30507c6
Show file tree
Hide file tree
Showing 12 changed files with 354 additions and 110 deletions.
8 changes: 8 additions & 0 deletions examples/active_window.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import map2

def on_window_change(active_window_class):
print("active window class: {}".format(active_window_class))


window = map2.Window()
window.on_window_change(on_window_change)
93 changes: 93 additions & 0 deletions examples/wasd_mouse_control.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
'''
Move the mouse using the 'w', 'a', 's', 'd' directional keys.
'''

import map2
import time
import subprocess
import threading

map2.default(layout = "us")

# an easy to use interval utility that allows us to run a function on a timer
class setInterval:
def __init__(self, interval, action):
self.interval = interval
self.action = action
self.stopEvent = threading.Event()
thread = threading.Thread(target = self.__setInterval)
thread.start()

def __setInterval(self):
nextTime = time.time() + self.interval
while not self.stopEvent.wait(nextTime - time.time()):
nextTime += self.interval
self.action()

def cancel(self):
self.stopEvent.set()

# read from input devices
reader_kbd = map2.Reader(patterns=["/dev/input/by-id/example-keyboard"])
reader_mouse = map2.Reader(patterns=["/dev/input/by-id/example-mouse"])

# add new virtual output devices
writer_kbd = map2.Writer(clone_from = "/dev/input/by-id/example-keyboard")
writer_mouse = map2.Writer(clone_from = "/dev/input/by-id/example-mouse")

# add mappers
mapper_kbd = map2.Mapper()
mapper_mouse = map2.Mapper()

# to move the mouse programmatically, we need a virtual reader
out_mouse = map2.VirtualReader()

# setup the event routing
map2.link([reader_kbd, mapper_kbd, writer_kbd])
map2.link([reader_mouse, mapper_mouse, writer_mouse])
map2.link([out_mouse, writer_mouse])


# we keep a map of intervals that maps each key to the associated interval
intervals = {}

def mouse_ctrl(key, state, axis, multiplier):
def inner_fn():
# if the key was released, remove and cancel the corresponding interval
if state == 0:
if key in intervals:
intervals.pop(key).cancel()
return

# this function will move our mouse using the virtual reader from earlier
def send():
value = 15 * multiplier
out_mouse.send("{{relative {} {}}}".format(axis, value))

# we call it once to move the mouse a bit immediately on key down
send()
# and register an interval that will continue to move it on a timer
intervals[key] = setInterval(0.02, send)
return inner_fn


# setup the key mappings
mapper_kbd.map("w up", mouse_ctrl("w", 0, "Y", -1))
mapper_kbd.map("w down", mouse_ctrl("w", 1, "Y", -1))
mapper_kbd.nop("w repeat")

mapper_kbd.map("a up", mouse_ctrl("a", 0, "X", -1))
mapper_kbd.map("a down", mouse_ctrl("a", 1, "X", -1))
mapper_kbd.nop("a repeat")

mapper_kbd.map("s up", mouse_ctrl("s", 0, "Y", 1))
mapper_kbd.map("s down", mouse_ctrl("s", 1, "Y", 1))
mapper_kbd.nop("s repeat")

mapper_kbd.map("d up", mouse_ctrl("d", 0, "X", 1))
mapper_kbd.map("d down", mouse_ctrl("d", 1, "X", 1))
mapper_kbd.nop("d repeat")


# Keep running forever
map2.wait()
8 changes: 5 additions & 3 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ use crate::*;
use crate::python::*;

#[derive(Error, Debug)]
pub enum InputError {
pub enum ApplicationError {
#[error("expected a callable object")]
NotCallable
NotCallable,
#[error("unsupported platform, supported platforms are: Hyprland")]
UnsupportedPlatform,
}

impl Into<PyErr> for InputError { fn into(self) -> PyErr { PyRuntimeError::new_err(self.to_string()) } }
impl Into<PyErr> for ApplicationError { fn into(self) -> PyErr { PyRuntimeError::new_err(self.to_string()) } }
5 changes: 3 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#![feature(fn_traits)]
#![feature(type_alias_impl_trait)]

#![recursion_limit = "256"]

#![allow(warnings)]
Expand Down Expand Up @@ -34,13 +36,11 @@ use crate::event::InputEvent;
pub use crate::key_defs::*;
use crate::key_primitives::*;
use crate::state::*;
use crate::x11::ActiveWindowInfo;
use crate::error::*;

// #[macro_use]
// use subscriber::linkable;

pub mod x11;
pub mod key_defs;
pub mod state;
pub mod key_primitives;
Expand All @@ -56,6 +56,7 @@ pub mod xkb;
pub mod xkb_transformer_registry;
pub mod error;
pub mod global;
pub mod platform;

#[cfg(feature = "integration")]
pub mod testing;
Expand Down
6 changes: 3 additions & 3 deletions src/mapper/mapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ impl Mapper {
*self.inner.fallback_handler.write().unwrap() = Some(fallback_handler);
return Ok(());
}
Err(InputError::NotCallable.into())
Err(ApplicationError::NotCallable.into())
}

pub fn map_relative(&mut self, py: Python, handler: PyObject) -> PyResult<()> {
Expand All @@ -405,7 +405,7 @@ impl Mapper {
*self.inner.relative_handler.write().unwrap() = Some(handler);
return Ok(());
}
Err(InputError::NotCallable.into())
Err(ApplicationError::NotCallable.into())
}

pub fn map_absolute(&mut self, py: Python, handler: PyObject) -> PyResult<()> {
Expand All @@ -415,7 +415,7 @@ impl Mapper {
*self.inner.absolute_handler.write().unwrap() = Some(handler);
return Ok(());
}
Err(InputError::NotCallable.into())
Err(ApplicationError::NotCallable.into())
}

pub fn nop(&mut self, from: String) -> PyResult<()> {
Expand Down
48 changes: 48 additions & 0 deletions src/platform/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use std::process::Command;

pub enum Platform {
Hyprland,
X11,
Unknown,
}


pub fn get_platform() -> Platform {
if platform_is_hyprland() {
return Platform::Hyprland;
}
if platform_is_x11() {
return Platform::X11;
}
Platform::Unknown
}

fn platform_is_hyprland() -> bool {
Command::new("printenv")
.arg("HYPRLAND_INSTANCE_SIGNATURE")
.status()
.map(|status| status.success())
.unwrap_or(false)
}

fn platform_is_x11() -> bool {
Command::new("printenv")
.arg("XDG_SESSION_TYPE")
.output()
.map(|info| info.status.success() && String::from_utf8_lossy(&info.stdout) == "x11")
.unwrap_or(false)
}

// fn platform_is_sway() -> bool {
// Command::new("printenv")
// .arg("SWAYSOCK")
// .status()
// .map(|status| status.success())
// .unwrap_or(false)
// }

// for kde/kwin (wayland)
// https://unix.stackexchange.com/questions/706477/is-there-a-way-to-get-list-of-windows-on-kde-wayland

// for gnome
// https://github.com/ActivityWatch/aw-watcher-window/pull/46/files
2 changes: 0 additions & 2 deletions src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@ use crate::*;

pub struct State {
pub modifiers: Arc<KeyModifierState>,
pub active_window: Option<ActiveWindowInfo>,
}


impl State {
pub fn new() -> Self {
State {
modifiers: Arc::new(KeyModifierState::new()),
active_window: None,
}
}
}
94 changes: 0 additions & 94 deletions src/window.rs

This file was deleted.

53 changes: 53 additions & 0 deletions src/window/hyprland_window.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
use std::panic::catch_unwind;
use crate::*;
use crate::python::*;
use hyprland::event_listener::EventListenerMutable as EventListener;
use crate::window::window_base::{ActiveWindowInfo, WindowControlMessage, WindowHandler};

pub fn hyprland_window_handler() -> WindowHandler {
Box::new(|exit_rx: oneshot::Receiver<()>,
subscription_rx: mpsc::Receiver<WindowControlMessage>| -> Result<()> {
let mut subscriptions = Arc::new(Mutex::new(HashMap::new()));

let prev_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(|_info| {}));

let mut event_listener = catch_unwind(|| EventListener::new())
.map_err(|err| anyhow!(
"hyprland connection error: {}",
err.downcast::<String>().unwrap_or(Box::new("unknown".to_string())
)))?;

std::panic::set_hook(prev_hook);

event_listener.add_active_window_change_handler(move |info, _| {
if exit_rx.try_recv().is_ok() { return; }

while let Ok(msg) = subscription_rx.try_recv() {
match msg {
WindowControlMessage::Subscribe(id, callback) => { subscriptions.lock().unwrap().insert(id, callback); }
WindowControlMessage::Unsubscribe(id) => { subscriptions.lock().unwrap().remove(&id); }
}
}

if let Some(info) = info {
let val = ActiveWindowInfo {
class: info.window_class.clone(),
instance: "".to_string(),
name: info.window_title.clone(),
};

Python::with_gil(|py| {
for callback in subscriptions.lock().unwrap().values() {
let is_callable = callback.as_ref(py).is_callable();
if !is_callable { continue; }

let _ = callback.call(py, (val.class.clone(), ), None);
}
});
}
});
event_listener.start_listener()?;
Ok(())
})
}
Loading

0 comments on commit 30507c6

Please sign in to comment.