├── requirements.txt ├── LICENSE ├── .gitignore ├── switch-layout.py └── README.md /requirements.txt: -------------------------------------------------------------------------------- 1 | pynput>=1.4 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Oleg Yamnikov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/python 3 | 4 | ### Python ### 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # celery beat schedule file 88 | celerybeat-schedule 89 | 90 | # SageMath parsed files 91 | *.sage.py 92 | 93 | # Environments 94 | .env 95 | .venv 96 | env/ 97 | venv/ 98 | ENV/ 99 | env.bak/ 100 | venv.bak/ 101 | 102 | # Spyder project settings 103 | .spyderproject 104 | .spyproject 105 | 106 | # Rope project settings 107 | .ropeproject 108 | 109 | # mkdocs documentation 110 | /site 111 | 112 | # mypy 113 | .mypy_cache/ 114 | .dmypy.json 115 | dmypy.json 116 | 117 | ### Python Patch ### 118 | .venv/ 119 | 120 | ### Python.VirtualEnv Stack ### 121 | # Virtualenv 122 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 123 | [Bb]in 124 | [Ii]nclude 125 | [Ll]ib 126 | [Ll]ib64 127 | [Ll]ocal 128 | [Ss]cripts 129 | pyvenv.cfg 130 | pip-selfcheck.json 131 | 132 | 133 | # End of https://www.gitignore.io/api/python 134 | 135 | # IDE files 136 | .idea/ 137 | .vscode/ 138 | -------------------------------------------------------------------------------- /switch-layout.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import subprocess 3 | 4 | from pynput import keyboard 5 | 6 | # The shortcuts triggering switch 7 | SWITCH_SHORTCUTS = [ 8 | # For some reason Alt pressed after Shift gets registered as a different 9 | # key code on my system. 10 | {keyboard.KeyCode(65511)}, # Shift + Alt 11 | {keyboard.Key.alt, keyboard.Key.shift}, # Alt + Shift 12 | 13 | # Examples 14 | # {keyboard.Key.shift, keyboard.Key.ctrl}, # Shift + Ctrl 15 | # {keyboard.Key.cmd, keyboard.Key.space}, # Super + Space 16 | # {keyboard.Key.caps_lock}, # CapsLock 17 | ] 18 | 19 | # How many layouts do you have? 20 | LAYOUTS_COUNT = 2 21 | 22 | # If you're having troubles configuring SWITCH_SHORTCUTS, set this to True. 23 | # Script will output pressed keys so you could copy-paste them. 24 | DEBUG = False 25 | 26 | 27 | def format_key(key): 28 | """ 29 | Formats a key the way it should be written in SWITCH_SHORTCUTS list. 30 | """ 31 | if isinstance(key, keyboard.Key): 32 | return "keyboard.Key.{}".format(key.name) 33 | else: 34 | return "keyboard.KeyCode({})".format(key.vk) 35 | 36 | 37 | class Switcher: 38 | def __init__(self): 39 | self.current_keys = set() 40 | self.keys_pressed = 0 41 | 42 | self.monitored_keys = set() 43 | for shortcut in SWITCH_SHORTCUTS: 44 | self.monitored_keys |= shortcut 45 | 46 | self.current_layout = 0 47 | 48 | def on_press(self, key): 49 | if DEBUG: 50 | print("Pressed: {}".format(format_key(key))) 51 | 52 | if key not in self.monitored_keys: 53 | return 54 | 55 | self.current_keys.add(key) 56 | self.keys_pressed += 1 57 | 58 | if self.is_switch_shortcut(): 59 | self.on_switch() 60 | 61 | def on_release(self, key): 62 | if DEBUG: 63 | print("Released: {}".format(format_key(key))) 64 | 65 | self.keys_pressed -= 1 66 | 67 | # Sometimes one key is pressed and another is released. 68 | # Blame X server. 69 | if key in self.current_keys: 70 | self.current_keys.remove(key) 71 | 72 | if self.keys_pressed <= 0: 73 | self.keys_pressed = 0 74 | self.current_keys = set() 75 | 76 | def is_switch_shortcut(self): 77 | for shortcut in SWITCH_SHORTCUTS: 78 | if self.current_keys.issuperset(shortcut): 79 | return True 80 | 81 | return False 82 | 83 | def on_switch(self): 84 | self.current_layout += 1 85 | if self.current_layout >= LAYOUTS_COUNT: 86 | self.current_layout = 0 87 | 88 | command = [ 89 | "gsettings", 90 | "set", 91 | "org.gnome.desktop.input-sources", 92 | "current", 93 | str(self.current_layout), 94 | ] 95 | _exitcode = subprocess.call( 96 | command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 97 | 98 | 99 | def main(): 100 | switcher = Switcher() 101 | 102 | with keyboard.Listener( 103 | on_press=switcher.on_press, 104 | on_release=switcher.on_release) as listener: 105 | listener.join() 106 | 107 | 108 | if __name__ == '__main__': 109 | main() 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # switch-layout 2 | 3 | ## What is this? 4 | 5 | This is a workaround for keyboard layout switching bug, which occurs, as far as I know, on some Gnome 3 based DEs running on X server, including Pantheon (elementary OS DE). According to some of the reports I've read, it affects mostly users of Russian keyboard layout. 6 | 7 | The bug is this: there is an annoying delay between the moment you try to switch layout (either with a shortcut or with the GUI) and the moment the layout is switched. In my case, it's around 1 second when switching from RU to EN. Sometimes the layout doesn't switch at all! This bug makes it impossible to type text both in Russian and English quickly. 8 | 9 | This workaround made things easier for me. It's not ideal, but it reduces the delay far enough to make it barely noticable. 10 | 11 | Related reports on Launchpad: 12 | 13 | * [Delay before you can type after switching input source](https://bugs.launchpad.net/ubuntu/+source/gnome-control-center/+bug/1754702/) 14 | * [Gnome on Xorg freezes for short time on every keyboard layout switch](https://bugs.launchpad.net/ubuntu/+source/ubiquity/+bug/1790335) 15 | * [layout switch is delayed](https://bugs.launchpad.net/ubuntu/+source/console-setup/+bug/1370953) 16 | 17 | ## How does it work 18 | 19 | I've found that I can switch my layout using `gsettings` command without any delay: 20 | 21 | ``` 22 | gsettings set org.gnome.desktop.input-sources current X 23 | ``` 24 | 25 | Where `X` is the index of a layout: `0` for the first one, `1` for the second, etc. 26 | 27 | This script listens to the switch shortcut and runs the command itself. 28 | 29 | ## How to install it 30 | 31 | 1. Make sure you have `pip` installed. 32 | 33 | ``` 34 | sudo apt install python3-pip 35 | ``` 36 | 37 | 2. Install [pynput](https://pypi.org/project/pynput/) - the script uses it to listen for key presses. 38 | 39 | ``` 40 | python3 -m pip install --user pynput==1.4 41 | ``` 42 | 43 | 3. Copy the `switch-layout.py` somewhere in your home folder. 44 | 4. Edit the `SWITCH_SHORTCUTS` and `LAYOUTS_COUNT` at the top of the script accordingly to your needs. 45 | 5. Disable built-in layout-switch shortcut in your system, so it wouldn't interfere with the script. Also disable CapsLock default behavior in case you use it as switch key. 46 | 6. Run the script: `python3 switch-layout.py`. Try pressing the shortcut and see if it works. 47 | If it doesn't, feel free to open an issue. Maybe I'll be able to help. 48 | Press `Ctrl+C` to stop the script. 49 | 7. If it works, you can add this script to autostart programs in your DE to make it start automatically. 50 | 51 | ## How to set `SWITCH_SHORTCUTS` 52 | 53 | Generally, you should be able to select one of the examples given in the comments 54 | of the script. But it might happen that a shortcut does not register on your system 55 | even if you put it into `SWITCH_SHORTCUTS`, because your key codes might not match. 56 | If that's the case, set the `DEBUG` constant to `True` and run the script from terminal: 57 | 58 | ```python 59 | DEBUG = True 60 | ``` 61 | 62 | This way all the pressed and released keys will be printed on the screen, so you 63 | can just copy them and paste into `SWITCH_SHORTCUTS`: 64 | 65 | ``` 66 | $ ./switch-layout.py 67 | Pressed: keyboard.Key.shift 68 | Pressed: keyboard.KeyCode(65511) 69 | Released: keyboard.Key.shift 70 | Released: keyboard.Key.alt 71 | ``` 72 | 73 | For example, in my case above Alt pressed after Shift got registered by code 74 | 65511. So I had to add it to one of my switch shortcuts to make it work. 75 | --------------------------------------------------------------------------------