├── .gitignore ├── LICENSE ├── README.md ├── RELEASING.md ├── pyproject.toml ├── qtile_mutable_scratch ├── _MutableScratch.py ├── __init__.py └── _version.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 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 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 James Wright 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # qtile MutableScratch 2 | 3 | [![PyPI](https://img.shields.io/pypi/v/qtile-mutable_scratch)](https://pypi.org/project/qtile-mutable-scratch/) 4 | 5 | This package is a series of functions and a class to create a "scratch" space 6 | in qtile more similar to i3's. qtile has the `ScratchPad` group type, but the 7 | (documented) purpose is to only host `Dropdown` windows that must be specified 8 | ahead of time. 9 | 10 | Instead, what `MutableScratch` does is piggybacks onto an "invisible" qtile 11 | `Group` (ie. a group named `''`) and provide functions to dynamically add and 12 | remove windows to this group. Viewing the "hidden" windows is done via a toggle 13 | function, which cycles through the windows in the Scratch group. All windows 14 | added to the `MutableScratch` group will be automatically converted to 15 | floating. This emulates the scratch functionality of i3 as closely as possible. 16 | 17 | See [repository README for most up-to-date documentation](https://github.com/jrwrigh/qtile-mutable-scratch). 18 | 19 | ## Installation 20 | 21 | You can now install via `pip`: 22 | ``` 23 | pip install qtile_mutable_scratch 24 | ``` 25 | 26 | ## Setup 27 | 28 | Put the following default configuration in your `config.py`: 29 | ```python 30 | import qtile_mutable_scratch 31 | from libqtile.config import EzKey 32 | from libqtile import hook 33 | ... 34 | 35 | mutscr = qtile_mutable_scratch.MutableScratch() 36 | groups.append(Group('')) # Must be after `groups` is created 37 | 38 | keys.extend( [ 39 | EzKey('M-S-', mutscr.add_current_window()), 40 | EzKey('M-C-', mutscr.remove_current_window()), 41 | EzKey('M-', mutscr.toggle()), 42 | ] ) 43 | 44 | hook.subscribe.startup_complete(mutscr.qtile_startup) 45 | ``` 46 | 47 | Each `MutableScratch` instance has two parameters to chose from, `scratch_group_name` and 48 | `win_attr`. 49 | It's not necessary to set these, as the default configuration 50 | should work for every configuration. 51 | `scratch_group_name` (default `''`, or the empty name group) sets the name of the group that will old the scratch windows. 52 | `win_attr` (default `mutscratch`) sets the attribute that will be set on each window to tag it as being apart of the `MutableScratch` system. 53 | 54 | ## Usage 55 | 56 | 1. Add the current window to the `MutableScratch` group via `MutableScratch.add_current_window()` 57 | - This will move the window to the invisible group 58 | 2. Rotate through windows in the `MutableScratch` group via `MutableScratch.toggle()` 59 | - If the current window is apart of the `MutableScratch` group, then it will be moved back to the invisible group 60 | - If the current window is not apart of the `MutableScratch`, then the next `MutableScratch` window in the stack will be moved to the current group 61 | 3. To remove a window from the `MutableScratch` group, use `MutableScratch.remove()` 62 | 63 | ### Hastily thrown together demo video: 64 | It's ugly, but it get's the point across...hopefully. 65 | 66 | https://user-images.githubusercontent.com/20801821/147259912-5acec613-239b-4fe3-aebb-9c1820426d2c.mp4 67 | 68 | 69 | ## Implementation Details 70 | 71 | ### Tracking members of the `MutableScratch` group 72 | 73 | This is done by dynamically adding an attribute (by default `mutscratch`) to 74 | the window object that simply stores a boolean. 75 | 76 | ### Cycling through windows in `MutableScratch` 77 | 78 | `MutableScratch` has something similar to qtile's `focus_history` for groups. 79 | It's effectively just a stack of windows belonging to the `MutableScratch` group, where windows are pushed and popped from the stack. 80 | Doing this,`MutableScratch` controls the order in which the windows are stored in the stack. 81 | This ensures that the every window in the `MutableScratch` group can be accessed via toggle. 82 | 83 | ### Initializing the `MutableScratch` on qtile start 84 | 85 | When restarting qtile, the `MutableScratch` instance in `config.py` will be overwritten, losing the stack history and the floating window status of it's windows. 86 | To stop this, we add a `hook` function to `startup_complete` that will reinitialize a new `MutableScratch` instance with the windows that are located in the `MutableScratch.scratch_group_name`. 87 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Steps for Release 2 | 3 | 0. Update version in `setup.py` 4 | 1. Build the wheels via `python -m build` 5 | - This will create files in a directory called `dist` 6 | 2. Upload the wheels to PyPI via `twine` 7 | - `python3 -m twine upload dist/*` 8 | - This will require an API token to perform the upload 9 | - Username is `__token__`, password is the token itself 10 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=42", 4 | "wheel" 5 | ] 6 | build-backend = "setuptools.build_meta" 7 | 8 | # [project] 9 | # name = "qtile-mutable_scratch" 10 | # authors = [{name = "James Wright", email = "james@jameswright.xyz"}] 11 | # description = "Add mutable scratch functionality to qtile" 12 | # readme = "README.md" 13 | # license = {file = "LICENSE"} 14 | # keywords = ["qtile"] 15 | # requires-python = ">=3.6" 16 | # classifiers = [ 17 | # "Programming Language :: Python :: 3", 18 | # "License :: OSI Approved :: MIT License", 19 | # "Operating System :: OS Independent", 20 | # ] 21 | # dependencies = [ 22 | # "qtile>=0.17" 23 | # ] 24 | # 25 | # # dynamic = ["version"] 26 | # 27 | # [project.urls] 28 | # documentation = "https://github.com/jrwrigh/qtile-mutable_scratch" 29 | # repository = "https://github.com/jrwrigh/qtile-mutable_scratch" 30 | # "Bug Tracker" = "https://github.com/jrwrigh/qtile-mutable_scratch/issues" 31 | # 32 | # # [tools.setuptools.dynamic] 33 | # # version = {attr = "qtile_mutable_scratch._version.__version__"} 34 | # 35 | # [tool.setuptools.packages.find] 36 | # include = [ 37 | # "qtile_mutable_scratch", 38 | # "qtile_mutable_scratch.*", 39 | # ] 40 | -------------------------------------------------------------------------------- /qtile_mutable_scratch/_MutableScratch.py: -------------------------------------------------------------------------------- 1 | import libqtile 2 | import libqtile.config 3 | from libqtile.lazy import lazy 4 | from libqtile.log_utils import logger 5 | 6 | # For type hints 7 | from libqtile.core.manager import Qtile 8 | from libqtile.group import _Group 9 | from libqtile.backend import base 10 | from collections.abc import Callable 11 | 12 | 13 | class MutableScratch(object): 14 | """For creating a mutable scratch workspace (similar to i3's scratch functionality)""" 15 | 16 | 17 | def __init__(self, win_attr: str='mutscratch', group_name: str=''): 18 | """ 19 | 20 | Parameters 21 | ---------- 22 | win_attr : str 23 | Attribute added to the qtile.window object to determine whether the 24 | window is a part of the MutableScratch system 25 | group_name : str 26 | Name of the group that holds the windows added to the scratch space 27 | """ 28 | 29 | self.win_attr: str = win_attr 30 | self.scratch_group_name: str = group_name 31 | 32 | self.win_stack: list = [] # Equivalent of focus_history 33 | 34 | 35 | def qtile_startup(self): 36 | """Initialize MutableScratch group on restarts 37 | 38 | Put 39 | hook.subscribe.startup_complete(.qtile_startup) 40 | in your config.py to initialize the windows in the MutScratch group 41 | """ 42 | 43 | qtile = libqtile.qtile 44 | group = qtile.groups_map[self.scratch_group_name] 45 | 46 | for win in group.windows: 47 | win.floating = True 48 | setattr(win, self.win_attr, True) 49 | 50 | self.win_stack = group.windows.copy() 51 | 52 | 53 | def add_current_window(self) -> Callable: 54 | """Add current window to the MutableScratch system""" 55 | @lazy.function 56 | def _add_current_window(qtile: Qtile): 57 | win: base.Window = qtile.current_window 58 | win.hide() 59 | win.floating = True 60 | setattr(win, self.win_attr, True) 61 | 62 | win.togroup(self.scratch_group_name) 63 | self.win_stack.append(win) 64 | 65 | return _add_current_window 66 | 67 | 68 | def remove_current_window(self) -> Callable: 69 | """Remove current window from MutableScratch system""" 70 | @lazy.function 71 | def _remove(qtile: Qtile): 72 | win = qtile.current_window 73 | setattr(win, self.win_attr, False) 74 | 75 | if win in self.win_stack: 76 | self.win_stack.remove(win) 77 | return _remove 78 | 79 | 80 | def toggle(self) -> Callable: 81 | """Toggle between hiding/showing MutableScratch windows 82 | 83 | If current window is in the MutableScratch system, hide the window. If 84 | it isn't, show the next window in the stack. 85 | """ 86 | @lazy.function 87 | def _toggle(qtile: Qtile): 88 | win: base.Window = qtile.current_window 89 | if getattr(win, self.win_attr, False): 90 | self._push(win) 91 | else: 92 | self._pop(qtile) 93 | return _toggle 94 | 95 | 96 | def _push(self, win: base.Window) -> None: 97 | """Hide and push window to stack 98 | 99 | Parameters 100 | ---------- 101 | win : libqtile.backend.base.Window 102 | Window to push to the stack 103 | """ 104 | win.togroup(self.scratch_group_name) 105 | win.keep_above(False) 106 | self.win_stack.append(win) 107 | 108 | 109 | def _pop(self, qtile: Qtile) -> None: 110 | """Show and pop window from stack 111 | 112 | Parameters 113 | ---------- 114 | qtile : libqtile.qtile 115 | qtile root object 116 | win : libqtile.backend.base.Window 117 | Window to pop from stack 118 | """ 119 | scratch_group: _Group = qtile.groups_map[self.scratch_group_name] 120 | if set(self.win_stack) != set(scratch_group.windows): 121 | logger.warning(f"{self}'s win_stack and {scratch_group}'s windows have mismatching windows: " 122 | f"{set(self.win_stack).symmetric_difference(set(scratch_group.windows))}") 123 | self.win_stack = scratch_group.windows.copy() 124 | if self.win_stack: 125 | win = self.win_stack.pop(0) 126 | win.togroup(qtile.current_group.name) 127 | win.keep_above(True) 128 | -------------------------------------------------------------------------------- /qtile_mutable_scratch/__init__.py: -------------------------------------------------------------------------------- 1 | from ._MutableScratch import MutableScratch 2 | from ._version import __version__ 3 | -------------------------------------------------------------------------------- /qtile_mutable_scratch/_version.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.0.3" 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r", encoding="utf-8") as fh: 4 | long_description = fh.read() 5 | 6 | with open("qtile_mutable_scratch/_version.py", "r") as fh: 7 | line = fh.read() 8 | exec(line) 9 | 10 | setuptools.setup( 11 | name="qtile-mutable_scratch", 12 | version=__version__, 13 | author="James Wright", 14 | author_email="james@jameswright.xyz", 15 | description="Add mutable scratch functionality to qtile", 16 | long_description=long_description, 17 | long_description_content_type="text/markdown", 18 | url="https://github.com/jrwrigh/qtile-mutable_scratch", 19 | project_urls={ 20 | "Bug Tracker": "https://github.com/jrwrigh/qtile-mutable_scratch/issues", 21 | }, 22 | classifiers=[ 23 | "Programming Language :: Python :: 3", 24 | "License :: OSI Approved :: MIT License", 25 | "Operating System :: OS Independent", 26 | ], 27 | packages=setuptools.find_packages(where="./qtile_mutable_scratch"), 28 | python_requires=">=3.6", 29 | ) 30 | --------------------------------------------------------------------------------