-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
18de671
commit a8e376d
Showing
6 changed files
with
162 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,34 @@ | ||
# miceless | ||
# MiceLess | ||
Utitlity that helps binding keyboard shortcuts for some of the operations that you do with mouse. | ||
|
||
# Usecase | ||
Primary usecase that this tool was written for is switching focus between windows on different monitors. | ||
In some cases, e.g. multiple desktops, simple `Alt-TAB` wont help, | ||
because it would either switch you to latest used app or would require you to press this combo several times, | ||
until you reach target window. With this tool you can configure shortcuts so that, for example, `Ctrl-Alt-1` would | ||
set window in left monitor in focus and `Ctrl+Alt+2` would set focus for window in right monitor. | ||
|
||
# Manual | ||
MiceLess stores a mapping between key-combos and sequences of mouse clicks. | ||
To run the app, execute `run.py` with `python3` interpreter. | ||
|
||
App config would be stored in home folder in `.miceless` file. | ||
|
||
The app should work anywhere where `pynput` works, yet it has been tested only on Ubuntu with X server. | ||
|
||
## Modes | ||
The tool has two operation modes: **recording** and **playback**. You can switch between modes by pressing `Ctrl+Alt+~`. | ||
|
||
## Recording | ||
While in **recording** mode, `Ctrl-Alt-<key>` combo would **append** click in current mouse location to the list | ||
of events for a given combo. Pressing special combination `Ctrl+Alt+0` would clear events list for last used key combo. | ||
|
||
## Playback | ||
In playback mode pressing `Ctrl+Alt+<key>` combo would invoke a sequence of events stored for that combination. | ||
|
||
# Known issues | ||
* Playback clicks are executed with `Ctrl+Alt` pressed, because all combos have these keys in them. | ||
This might be a problem for some applications. | ||
* There might be collisions with app-specific hotkeys that would result in not desired firings of event sequences. | ||
* Events capture is based on `pynput` which in turn uses `Xlib`, thus there might be issues in non-X environments, | ||
e.g. in Wayland. |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
#!/usr/bin/python3 | ||
import logging | ||
import os | ||
from pynput import keyboard | ||
|
||
from miceless.state import StateTracker | ||
|
||
|
||
if __name__ == "__main__": | ||
logging.basicConfig(level=logging.INFO, handlers=[logging.StreamHandler()]) | ||
state = StateTracker(os.path.join(os.getenv('HOME'), '.miceless')) | ||
|
||
with keyboard.Listener( | ||
on_press=state.pressed, | ||
on_release=state.released) as listener: | ||
listener.join() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import json | ||
import logging | ||
from pynput import keyboard, mouse | ||
from time import sleep | ||
|
||
|
||
class StateTracker: | ||
def __init__(self, config_path): | ||
self._config_path = config_path | ||
self._logger = logging.getLogger(__name__) | ||
self._is_r_alt = False | ||
self._is_r_ctrl = False | ||
self._is_rec_mode = False | ||
self._mouse = mouse.Controller() | ||
self._memory = {} | ||
self.load() | ||
self._last_key = None | ||
for k, v in self._memory.items(): | ||
self._logger.info(f'Loaded bind {k} = {v}') | ||
self._logger.info(f'Initialized in {"REC" if self._is_rec_mode else "PLAY"}') | ||
|
||
def check_modifiers(self): | ||
return self._is_r_alt and self._is_r_ctrl | ||
|
||
def dump(self): | ||
with open(self._config_path, 'w') as f: | ||
json.dump(self._memory, f) | ||
|
||
def load(self): | ||
try: | ||
with open(self._config_path, 'r') as f: | ||
self._memory = json.load(f) | ||
self._logger.info(f'Loaded {self._config_path}') | ||
except FileNotFoundError: | ||
self._logger.info('Nothing to load') | ||
|
||
@staticmethod | ||
def is_key_allowed(key): | ||
return len(str(key)) == 3 | ||
|
||
def pressed(self, key): | ||
if key == keyboard.Key.alt_r: | ||
self.set_r_alt(True) | ||
elif key == keyboard.Key.ctrl_r: | ||
self.set_r_ctrl(True) | ||
|
||
def released(self, key): | ||
if key == keyboard.Key.alt_r: | ||
self.set_r_alt(False) | ||
elif key == keyboard.Key.ctrl_r: | ||
self.set_r_ctrl(False) | ||
if self.check_modifiers() and self.is_key_allowed(key): | ||
if key == keyboard.KeyCode.from_char(char='`'): | ||
# Toggle mode. | ||
self._is_rec_mode = not self._is_rec_mode | ||
self._logger.info(f'Mode set to {"REC" if self._is_rec_mode else "PLAY"}') | ||
elif self._is_rec_mode: | ||
if key == keyboard.KeyCode.from_char(char='0'): | ||
if self._last_key.char in self._memory: | ||
del self._memory[self._last_key.char] | ||
self.dump() | ||
self._logger.info(f'Cleared key {self._last_key}') | ||
else: | ||
if key.char not in self._memory: | ||
self._memory[key.char] = [] | ||
self._memory[key.char].append(self._mouse.position) | ||
self.dump() | ||
self._logger.info(f'Recorded position {self._memory[key.char]} for key {key}') | ||
elif key.char in self._memory: | ||
self._logger.info(f'Executing {len(self._memory)} actions for key {key}') | ||
for pos in self._memory[key.char]: | ||
self._mouse.position = pos | ||
self._mouse.click(mouse.Button.left) | ||
self._logger.info(f'Clicked: {pos}') | ||
sleep(0.2) | ||
self._last_key = key | ||
|
||
def set_r_alt(self, state): | ||
self._is_r_alt = state | ||
|
||
def set_r_ctrl(self, state): | ||
self._is_r_ctrl = state |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
pynput==1.4 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import setuptools | ||
|
||
|
||
with open('README.md', 'r') as f: | ||
long_description = f.read() | ||
|
||
|
||
with open('requirements.txt', 'r') as f: | ||
requirements = f.readlines() | ||
|
||
|
||
setuptools.setup( | ||
name='miceless', | ||
version='1.0.0', | ||
maintainer='Dmytro Tkanov', | ||
maintainer_email='[email protected]', | ||
license='Apache License 2.0', | ||
description='Hotkeys for mouse click events.', | ||
long_description=long_description, | ||
long_description_content_type="text/markdown", | ||
url='https://github.com/akademi4eg/miceless', | ||
packages=setuptools.find_packages(), | ||
install_requires=requirements, | ||
classifiers=[ | ||
"Programming Language :: Python :: 3", | ||
"License :: OSI Approved :: Apache Software License", | ||
"Operating System :: POSIX :: Linux", | ||
"Topic :: Multimedia :: Sound/Audio", | ||
], | ||
) |