├── .codeclimate.yml ├── .coveragerc ├── .gitignore ├── .scrutinizer.yml ├── .travis.yml ├── AUTHORS ├── CONTRIBUTING.rst ├── ChangeLog ├── LICENSE ├── MANIFEST.in ├── README.rst ├── debian ├── changelog ├── compat ├── control ├── copyright ├── docs ├── rules └── source │ └── format ├── dev_requirements.txt ├── docs ├── Makefile ├── _static │ ├── contrib_box │ │ ├── bitcoin.png │ │ ├── bug.png │ │ ├── flattr.png │ │ ├── github.png │ │ └── paypal.png │ ├── custom.css │ ├── img │ │ ├── debian_12.png │ │ ├── mint_12.png │ │ └── ubuntu_12.png │ └── license.svg ├── _templates │ ├── about.html │ ├── donate.html │ ├── layout.html │ └── sidebar_badges.html ├── animation │ ├── .gitignore │ ├── Makefile │ ├── animation.gif │ └── svg │ │ ├── animation_01.svg │ │ ├── animation_02.svg │ │ ├── animation_03.svg │ │ ├── animation_04.svg │ │ ├── animation_05.svg │ │ ├── animation_06.svg │ │ ├── animation_07.svg │ │ ├── animation_08.svg │ │ ├── animation_09.svg │ │ ├── animation_10.svg │ │ ├── animation_11.svg │ │ ├── animation_12.svg │ │ ├── animation_13.svg │ │ ├── animation_14.svg │ │ ├── animation_15.svg │ │ ├── animation_16.svg │ │ ├── animation_17.svg │ │ ├── animation_18.svg │ │ ├── animation_19.svg │ │ ├── animation_20.svg │ │ ├── animation_21.svg │ │ ├── animation_22.svg │ │ ├── animation_23.svg │ │ └── animation_24.svg ├── apidocs │ ├── __main__.rst │ ├── commands.rst │ ├── config.rst │ ├── dbus_api.rst │ ├── functional_harness │ │ ├── env_general.rst │ │ └── x_server.rst │ ├── gtkexcepthook.rst │ ├── index.rst │ ├── keybinder.rst │ ├── layout.rst │ ├── test_functional.rst │ ├── test_util.rst │ ├── tests.rst │ ├── util.rst │ └── wm.rst ├── authors │ └── index.rst ├── cli.rst ├── commands.rst ├── conf.py ├── config.rst ├── deploy.sh ├── developing.rst ├── diagrams │ ├── .gitignore │ ├── Makefile │ ├── README.md │ ├── png │ │ ├── all-desktops.png │ │ ├── always-above.png │ │ ├── always-below.png │ │ ├── bordered.png │ │ ├── bottom-left.png │ │ ├── bottom-right.png │ │ ├── bottom.png │ │ ├── center.png │ │ ├── fullscreen.png │ │ ├── horizontal-maximize.png │ │ ├── left.png │ │ ├── maximize.png │ │ ├── minimize.png │ │ ├── monitor-next-all.png │ │ ├── monitor-next.png │ │ ├── monitor-prev-all.png │ │ ├── monitor-prev.png │ │ ├── move-to-bottom-left.png │ │ ├── move-to-bottom-right.png │ │ ├── move-to-bottom.png │ │ ├── move-to-center.png │ │ ├── move-to-left.png │ │ ├── move-to-right.png │ │ ├── move-to-top-left.png │ │ ├── move-to-top-right.png │ │ ├── move-to-top.png │ │ ├── right.png │ │ ├── shade.png │ │ ├── show-desktop.png │ │ ├── top-left.png │ │ ├── top-right.png │ │ ├── top.png │ │ ├── trigger-move.png │ │ ├── trigger-resize.png │ │ ├── vertical-maximize.png │ │ ├── workspace-go-down.png │ │ ├── workspace-go-left.png │ │ ├── workspace-go-next.png │ │ ├── workspace-go-prev.png │ │ ├── workspace-go-right.png │ │ ├── workspace-go-up.png │ │ ├── workspace-send-down.png │ │ ├── workspace-send-left.png │ │ ├── workspace-send-next.png │ │ ├── workspace-send-prev.png │ │ ├── workspace-send-right.png │ │ └── workspace-send-up.png │ └── svg │ │ ├── all-desktops.svg │ │ ├── always-above.svg │ │ ├── always-below.svg │ │ ├── bordered.svg │ │ ├── bottom-left.svg │ │ ├── bottom-right.svg │ │ ├── bottom.svg │ │ ├── center.svg │ │ ├── fullscreen.svg │ │ ├── horizontal-maximize.svg │ │ ├── left.svg │ │ ├── maximize.svg │ │ ├── minimize.svg │ │ ├── monitor-next-all.svg │ │ ├── monitor-next.svg │ │ ├── monitor-prev-all.svg │ │ ├── monitor-prev.svg │ │ ├── move-to-bottom-left.svg │ │ ├── move-to-bottom-right.svg │ │ ├── move-to-bottom.svg │ │ ├── move-to-center.svg │ │ ├── move-to-left.svg │ │ ├── move-to-right.svg │ │ ├── move-to-top-left.svg │ │ ├── move-to-top-right.svg │ │ ├── move-to-top.svg │ │ ├── right.svg │ │ ├── shade.svg │ │ ├── show-desktop.svg │ │ ├── top-left.svg │ │ ├── top-right.svg │ │ ├── top.svg │ │ ├── trigger-move.svg │ │ ├── trigger-resize.svg │ │ ├── vertical-maximize.svg │ │ ├── workspace-go-down.svg │ │ ├── workspace-go-left.svg │ │ ├── workspace-go-next.svg │ │ ├── workspace-go-prev.svg │ │ ├── workspace-go-right.svg │ │ ├── workspace-go-up.svg │ │ ├── workspace-send-down.svg │ │ ├── workspace-send-left.svg │ │ ├── workspace-send-next.svg │ │ ├── workspace-send-prev.svg │ │ ├── workspace-send-right.svg │ │ └── workspace-send-up.svg ├── faq.rst ├── index.rst ├── installation.rst ├── make.bat ├── publish-key.enc ├── update_authors.sh ├── usage.rst └── wrench.png ├── functional_harness ├── __init__.py ├── env_general.py └── x_server.py ├── icon.svg ├── install.sh ├── pylintrc ├── quicktile.desktop ├── quicktile.sh ├── quicktile ├── __init__.py ├── __main__.py ├── commands.py ├── config.py ├── dbus_api.py ├── gtkexcepthook.py ├── keybinder.py ├── layout.py ├── util.py ├── version.py └── wm.py ├── recompile_local_debian_package.sh ├── run_tests.sh ├── setup.cfg ├── setup.py ├── test_functional.py ├── tests ├── __init__.py ├── test_quicktile.py └── test_util.py └── tox.ini /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | duplication: 3 | enabled: true 4 | config: 5 | languages: 6 | - python 7 | exclude_fingerprints: 8 | - defa796682f05173a02dd24ce05faff8 9 | fixme: 10 | enabled: true 11 | # TODO: Figure out whether this is the correct syntax. They don't document it 12 | #pep8: 13 | # enabled: true 14 | # ignore: N802,E126,E128,E221,E302,E401,E402 15 | radon: 16 | enabled: true 17 | ratings: 18 | paths: 19 | - "**.py" 20 | exclude_paths: [] 21 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | omit = 3 | */python?.?/* 4 | */lib-python/?.?/*.py 5 | */lib_pypy/_*.py 6 | */site-packages/* 7 | */dist-packages/* 8 | */pyshared/* 9 | */gi/* 10 | */Xlib/* 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | .coverage 3 | dist 4 | docs/_build 5 | .mypy_cache 6 | _site 7 | .tox 8 | QuickTile.egg-info 9 | *.py[co] 10 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | checks: 2 | python: 3 | code_rating: true 4 | duplicate_code: true 5 | variables_used_before_assignment: true 6 | variables_unpacking_non_sequence: true 7 | variables_undefined_loop_variable: true 8 | variables_undefined_all_variable: true 9 | variables_unbalanced_tuple_unpacking: true 10 | basic_exec_used: true 11 | basic_eval_used: true 12 | variables_no_name_in_module: true 13 | variables_global_variable_undefined: true 14 | typecheck_unexpected_keyword_arg: true 15 | typecheck_too_many_function_args: true 16 | typecheck_not_callable: true 17 | typecheck_no_value_for_parameter: true 18 | typecheck_no_member: true 19 | typecheck_duplicate_keyword_arg: true 20 | typecheck_assignment_from_no_return: true 21 | string_too_many_format_args: true 22 | string_constant_anomalous_unicode_escape_in_string: true 23 | open_mode_bad_open_mode: true 24 | logging_too_many_args: true 25 | imports_import_self: true 26 | exceptions_raising_non_exception: true 27 | exceptions_raising_bad_type: true 28 | exceptions_catching_non_exception: true 29 | variables_redefined_builtin: true 30 | classes_protected_access: true 31 | classes_non_iterator_returned: true 32 | classes_no_self_argument: true 33 | classes_bad_mcs_classmethod_argument: true 34 | classes_bad_context_manager: true 35 | classes_bad_classmethod_argument: true 36 | basic_return_in_init: true 37 | classes_access_member_before_definition: true 38 | basic_not_in_loop: true 39 | basic_lost_exception: true 40 | basic_bad_reversed_sequence: true 41 | basic_dangerous_default_value: true 42 | basic_abstract_class_instantiated: true 43 | variables_unused_import: true 44 | variables_global_variable_not_assigned: true 45 | variables_global_statement: true 46 | variables_global_at_module_level: true 47 | typecheck_redundant_keyword_arg: true 48 | variables_undefined_variable: true 49 | string_constant_anomalous_backslash_in_string: true 50 | newstyle_bad_super_call: true 51 | imports_reimported: true 52 | imports_deprecated_module: true 53 | imports_cyclic_import: true 54 | exceptions_pointless_except: true 55 | exceptions_bare_except: true 56 | classes_method_hidden: true 57 | classes_bad_staticmethod_argument: true 58 | basic_unreachable: true 59 | basic_unnecessary_pass: true 60 | basic_expression_not_assigned: true 61 | basic_pointless_statement: true 62 | variables_unused_wildcard_import: true 63 | variables_redefine_in_handler: true 64 | imports_wildcard_import: true 65 | format_unnecessary_semicolon: true 66 | format_trailing_whitespace: true 67 | format_mixed_indentation: true 68 | format_missing_final_newline: true 69 | format_line_too_long: 70 | max_length: '80' 71 | format_bad_whitespace: true 72 | format_bad_indentation: 73 | indentation: '4 spaces' 74 | exceptions_notimplemented_raised: true 75 | classes_no_self_use: true 76 | basic_unnecessary_lambda: true 77 | basic_pointless_string_statement: true 78 | basic_invalid_name: 79 | functions: '[a-z_][a-z0-9_]{2,30}$' 80 | variables: '[a-z_][a-z0-9_]{2,30}$' 81 | whitelisted_names: 'i,j,k,ex,Run,_' 82 | constants: '(([A-Z_][A-Z0-9_]*)|(__.*__))$' 83 | attributes: '[a-z_][a-z0-9_]{2,30}$' 84 | arguments: '[a-z_][a-z0-9_]{2,30}$' 85 | class_attributes: '([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$' 86 | inline_vars: '[A-Za-z_][A-Za-z0-9_]*$' 87 | classes: '[A-Z_][a-zA-Z0-9]+$' 88 | modules: '(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$' 89 | methods: '[a-z_][a-z0-9_]{2,30}$' 90 | basic_empty_docstring: true 91 | variables_redefined_outer_name: true 92 | format_multiple_statements: true 93 | basic_function_redefined: true 94 | 95 | filter: 96 | excluded_paths: 97 | - '*/test/*' 98 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: python 3 | cache: pip 4 | python: 5 | - 3.5 6 | before_install: 7 | - python --version 8 | - pip install -U pip 9 | - pip install ghp-import 10 | virtualenv: 11 | system_site_packages: true 12 | addons: 13 | apt: 14 | packages: 15 | - python3-dbus 16 | - python3-gi 17 | - python3-nose 18 | - python3-coverage 19 | - python3-xlib 20 | - gir1.2-glib-2.0 21 | - gir1.2-gtk-3.0 22 | - gir1.2-wnck-3.0 23 | install: "pip3 install mypy coveralls" 24 | env: 25 | - MYPYPATH="quicktile" 26 | script: 27 | - mypy --no-warn-unused-ignores quicktile ./*.py functional_harness 28 | - nosetests3 29 | - python3 -m doctest quicktile/util.py 30 | after_success: coveralls 31 | deploy: 32 | provider: script 33 | script: docs/deploy.sh 34 | on: 35 | branch: master 36 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Authors 2 | ******* 3 | 4 | IMPORTANT: THIS DOCUMENT IS AUTOGENERATED. To update it, modify 5 | docs/authors/index.rst in the QuickTile source distribution and run 6 | docs/update_authors.sh 7 | 8 | NOTE: Due to limitations in Sphinx's generation of text output, 9 | attribution URLs are missing from this version and the textual 10 | rendering of inline images is less than ideal. The HTML version 11 | included in the QuickTile manual should be preferred where possible. 12 | 13 | 14 | The Program 15 | =========== 16 | 17 | QuickTile is primarily the work of Stephan Sokolow, however, it has 18 | received various contributions over the years. 19 | 20 | Thanks go out to the following people: 21 | 22 | David Stygstra 23 | Generalized the "move-to-center" command into the "move-to-*" 24 | family of commands. 25 | 26 | Fábio C. Barrionuevo da Luz 27 | Fixed some permissioning warts in the install procedure. 28 | 29 | Fritz Reichwald 30 | Added "Gtk.init_check" in "quicktile.__main__" to work around a bug 31 | in some builds of GTK as well as a "gi.require_version" that is 32 | only necessary on some systems. 33 | 34 | Gui Ambros 35 | Identified the solution for a misalignment that occurs on HiDPI 36 | screens. 37 | 38 | Gustavo J A M Carneiro and Filip Van Raemdonck 39 | Created the "gtkexcepthook" module in the form I started from. 40 | 41 | Justin 42 | Added the "horizontal-maximize", "vertical-maximize", and "move-to- 43 | center" commands. 44 | 45 | Matthias Putz 46 | Fixed use of an uninitialized variable in a difficult-to-trigger 47 | failure case. 48 | 49 | Max Weiß 50 | 51 | * Added the "KEYLOOKUP" dict to compensate for un-intuitive 52 | omissions for the symbol names of common keys. 53 | 54 | * Reworked the shebang to address the period when some Linux 55 | distros lacked a **python2** binary while others linked 56 | **python** to Python 3. 57 | 58 | Oliver Gerlich 59 | Corrected an omission on the list of dependencies for CentOS 7 and 60 | possibly other Red Hat-family distros. 61 | 62 | Stéphane Gourichon 63 | 64 | * Researched how to create ".deb" packages 65 | 66 | * Wrote "recompile_local_debian_package.sh" 67 | 68 | * Set up the necessary package metadata for building a ".deb". 69 | 70 | Stuart Axelbrooke 71 | Corrected a flaw in the window-tiling heuristics which prevented 72 | them from functioning correctly on especially large monitors. 73 | 74 | Thomas Vander Stichele 75 | Helped to clean up the API documentation during the ePyDoc era. 76 | 77 | Valdis Vitolins 78 | Added an omitted dependency on "python-setuptools" to "README.rst" 79 | 80 | Valentin Agachi 81 | Corrected window-layout calculations to use "round()" for pixel 82 | values. 83 | 84 | Yuting/Tim Xiao 85 | Made the window-tiling heuristics more robust. 86 | 87 | 88 | The Manual 89 | ========== 90 | 91 | With the following exceptions, all text and illustrations in the 92 | manual are copyright Stephan Sokolow. 93 | 94 | It should be assumed that, as the contents of the API documentation 95 | section are extracted from the source code, the copyright of a given 96 | entry will be the same as the code it describes. 97 | 98 | The text of the Alabaster theme for Sphinx, and any generated text 99 | inserted by Sphinx are copyright their respective rightsholders. 100 | 101 | 102 | Imagery 103 | ------- 104 | 105 | The following illustrations are copyright David Stygstra: 106 | 107 | * "bordered.svg" 108 | 109 | * "bottom-left.svg" 110 | 111 | * "bottom-right.svg" 112 | 113 | * "bottom.svg" 114 | 115 | * "fullscreen.svg" 116 | 117 | * "horizontal-maximize.svg" 118 | 119 | * "left.svg" 120 | 121 | * "maximize.svg" 122 | 123 | * "middle.svg" 124 | 125 | * "minimize.svg" 126 | 127 | * "move-to-bottom-left.svg" 128 | 129 | * "move-to-bottom-right.svg" 130 | 131 | * "move-to-bottom.svg" 132 | 133 | * "move-to-center.svg" 134 | 135 | * "move-to-left.svg" 136 | 137 | * "move-to-right.svg" 138 | 139 | * "move-to-top-left.svg" 140 | 141 | * "move-to-top-right.svg" 142 | 143 | * "move-to-top.svg" 144 | 145 | * "right.svg" 146 | 147 | * "shade.svg" 148 | 149 | * "top-left.svg" 150 | 151 | * "top-right.svg" 152 | 153 | * "top.svg" 154 | 155 | * "vertical-maximize.svg" 156 | 157 | [image: bug.png][image] and [image: wrench.png][image] from the Silk 158 | Icons set by Mark James are used under the Creative Commons 159 | Attribution 2.5 license. 160 | 161 | The [image: GPLv2+ License][image] badge is a locally cached copy of 162 | an SVG file generated by Shields.io. 163 | 164 | All other favicons and logos are copyright their respective owners and 165 | used only to display favicon-style links to their owners' websites. 166 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Before You Open An Issue... 2 | =========================== 3 | 4 | 1. Please check the `FAQ `_ to make 5 | sure it isn't a known problem or a feature that's been declared out-of-scope 6 | for QuickTile. 7 | 8 | 2. Try the newest version of QuickTile to see if it's already been fixed/added. 9 | 10 | 3. Search for open bugs to verify that someone else hasn't already reported 11 | the problem. 12 | 13 | 4. If you're reporting a bug, please attach the terminal output from running 14 | QuickTile with ``--debug`` and then triggering the problem. 15 | 16 | 5. If you're reporting a bug that resulted in the "A programming error has been 17 | detected during the execution of this program." dialog, please attach the 18 | traceback from the ``Details...`` button. 19 | 20 | Thanks for making it easier for me to help you. 21 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | 0.4.1 (git HEAD): 2 | - Add support for HiDPI monitors where GDK's pixel units don't necessarily 3 | match Wnck pixel units by default. 4 | - Add minimal support for querying _GTK_WORKAREAS_D0 (GNOME Shell on X11) 5 | - Expose existing margin support via config file 6 | - Add a generic exception catcher for keybindings so errors in tiling commands 7 | shouldn't render QuickTile non-responsive. 8 | - Work around inherent race condition in gathering strut reservations 9 | - Fix some PyGI-related warnings that only happen on certain systems 10 | - Adjust installation instruction/scripts to avoid creating root-permissioned 11 | pip cache files in the user's home directory. 12 | - Assorted documentation improvements. 13 | - Continued efforts to increase automated testing 14 | 15 | 0.4.0: 16 | - Port to Python 3.x and GTK 3.x 17 | - Switch command-line arguments from optparse to argparse 18 | - Convert the API docs from ePyDoc to Sphinx and add a manual 19 | - Add illustrations for all commands in the command reference 20 | - Rename the `middle` command to `center` for consistency with `move-to-*` 21 | - Ensure off-screen windows can be manipulated relative to the nearest monitor 22 | - Ensure monitor-* commands don't place windows off-screen 23 | - Make move-to-* commands center non-corner destinations for consistency 24 | - Have move-to-* preserve horizontal/vertical maximization state 25 | - Add default keybindings for move-to-* commands 26 | - Fix issue #45 (support desktops where panels reserve space on interior edges) 27 | - Significant internal reworking to make the codebase more maintainable 28 | - More FAQ entries 29 | - Much more automated testing 30 | - Create a (basic) icon for QuickTile 31 | - Add infrastructure for building Debian packages (Thanks to Stéphane Gourichon) 32 | 33 | 0.3.0: 34 | - Split QuickTile up into multiple files for easier refactoring and improvement 35 | - Fix a few latent WindowManager bugs revealed by the split-up. 36 | - Add a `ColumnCount` field to the config file 37 | - Add a `MovementsWrap` field to the config file to control wrapping in 38 | `-next` and `-previous` movements 39 | - Allow `ModMask` to be empty/None to enable bindings without a common prefix 40 | - Add `monitor-prev-all`, `monitor-next-all`, and `monitor-switch-all` commands 41 | - Add gtkexcepthook for more convenient handling of unexpected errors 42 | - Fix window cycling when windows impose constraints on their dimensions 43 | - Work around PyGTK's incompatibilty with tox 44 | - Make install.sh a bit more robust 45 | - Do a ton of refactoring to ease reusing existing code in new commands. 46 | 47 | 0.2.2: 48 | - Added move-to{,-top,-bottom}{,-left,-right} (David Stygstra) 49 | - Implement window gravity support in reposition() 50 | - Allow reposition() to take a custom geometry mask 51 | - Begin implementing the basics of a unit test suite 52 | - Actually make cycle_monitors() obey the step argument 53 | - Don't allow selection of the desktop window 54 | - Added a safety check for the use of in modmask 55 | - Extend GravityLayout to support margins 56 | - More helpful --debug messages 57 | - Rewrite README in ReStructuredText 58 | - Add static analysis hooks to repo 59 | - Various code cleanups 60 | - Modernize setup.py and remove absolute install path blocking packaging 61 | - Fix install.sh to ensure it's operating on the correct path 62 | - Use round for pixel numbers calculations 63 | - Fix left-right size cycling for large monitors 64 | 65 | 0.2.1: 66 | - Added monitor-next and monitor-previous 67 | - Added all-desktops, pin, fullscreen, always-above, always-below, and shade 68 | - Added workspace-{go,send}-{next,prev,up,down,left,right} 69 | - Added minimize and bordered commands 70 | - Added commands for triggering keyboard-driven move/resize 71 | - Switch to gtk.accelerator_parse() for keycode lookup 72 | - Removed AltGr from the list of ignored modifiers 73 | - Fix detection for partial maximization 74 | 75 | 0.2.0.1: 76 | - Bugfix for a mistake introduced while refactoring the command-line help 77 | 78 | 0.2.0: 79 | - Switch to programmatic generation of the presets table 80 | - Rely more fully on libwnck to minimize NIH-related bugs 81 | 82 | 0.1.6: 83 | - Use libwnck for various operations to avoid bugs 84 | - Audited and fixed up the API documentation 85 | 86 | 0.1.5: 87 | - Keybindings and base modifier key can now be set via ~/.config/quicktile.cfg 88 | - Added a setup.py capable of installing and setting up autostart behaviour. 89 | - Added a --no-workarea flag and config file key for non-rectangular desktops 90 | - Added a --show-bindings option to list keybindings 91 | - Added horizontal-maximize, vertical-maximize, and move-to-center (crantok) 92 | - Also tile windows which don't implement _NET_WM_WINDOW_TYPE 93 | - Relax cycleDimensions matching to avoid edge cases (Yuting Xiao) 94 | - Switch the shebang to use "python2" for more compatibility 95 | - Improved the error handling with more helpful messages 96 | - More internal restructuring and code clean-ups 97 | - Tidied up the --help output a bit 98 | - Write a proper README in Markdown 99 | 100 | 0.1.4: 101 | - Much internal restructuring 102 | - Docstrings reformatted for epydoc 3.x 103 | - Renamed --bindkeys to --daemonize (--bindkeys usable but deprecated) 104 | - Added a D-Bus API which uses the same commands as the command-line API. 105 | - Fixed Exceptions when trying to use QuickTile on windows like MPlayer. 106 | - Fixed NumLock and CapsLock compatibility in the internal keybinder. 107 | - Workaround for windows getting stuck maximized. 108 | - Use _NET_WORKAREA to avoid getting covered by panels. 109 | 110 | 0.1.3: 111 | - Last version offered via ssokolow.com/scripts/ 112 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include quicktile.desktop 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | QuickTile 2 | ========= 3 | 4 | .. image:: https://scrutinizer-ci.com/g/ssokolow/quicktile/badges/quality-score.png?b=master 5 | :target: https://scrutinizer-ci.com/g/ssokolow/quicktile/?branch=master 6 | :alt: Scrutinizer Code Quality 7 | 8 | .. image:: https://codeclimate.com/github/ssokolow/quicktile/badges/gpa.svg 9 | :target: https://codeclimate.com/github/ssokolow/quicktile 10 | :alt: Code Climate 11 | 12 | .. image:: https://travis-ci.org/ssokolow/quicktile.svg?branch=master 13 | :target: https://travis-ci.org/ssokolow/quicktile 14 | :alt: Travis-CI 15 | 16 | .. image:: https://coveralls.io/repos/github/ssokolow/quicktile/badge.svg?branch=master 17 | :target: https://coveralls.io/github/ssokolow/quicktile?branch=master 18 | :alt: Coveralls 19 | 20 | .. image:: https://img.shields.io/badge/License-GPLv2%2B-blue 21 | :target: https://www.gnu.org/licenses/gpl-2.0.html 22 | :alt: License: GPLv2+ 23 | 24 | Keyboard-driven Window Tiling for your existing X11 window manager 25 | 26 | Important Message For Users of GTK+ 2.x QuickTile 27 | ------------------------------------------------- 28 | 29 | In addition to migrating from GTK+ 2.x to GTK 3, QuickTile 0.4.0 makes the 30 | following changes: 31 | 32 | * The PyGTK to PyGI migration has changed the list of dependencies 33 | significantly. 34 | * Regressions in certain GDK APIs have made python-xlib a mandatory dependency. 35 | * The ``middle`` command has been renamed to ``center`` for consistency with 36 | ``move-to-center``. You will have to update anything which calls ``middle`` 37 | via the command-line or D-Bus APIs, but ``quicktile.cfg`` will update 38 | automatically. 39 | * Please contact me if you maintain your own QuickTile patches. 40 | I have begun a major refactoring and want to make sure your changes 41 | get updated accordingly. 42 | 43 | Requirements: 44 | ------------- 45 | 46 | **Debian and derivatives (Ubuntu, Mint, etc.):** 47 | 48 | .. code:: sh 49 | 50 | sudo apt-get install python3 python3-pip python3-setuptools python3-gi python3-xlib python3-dbus gir1.2-glib-2.0 gir1.2-gtk-3.0 gir1.2-wnck-3.0 51 | 52 | **Fedora and derivatives:** 53 | 54 | .. code:: sh 55 | 56 | sudo dnf install python3 python3-pip python3-setuptools python3-gobject python3-xlib python3-dbus gtk3 libwnck3 57 | 58 | For other distros or for more details, please consult the `Dependencies 59 | `_ section of the 60 | manual. 61 | 62 | Installation 63 | ------------ 64 | 65 | QuickTile can be run from the source folder without installation via the 66 | ``./quicktile.sh`` script. 67 | 68 | For system-wide installation, the recommended option is ``pip3``, which will 69 | record a log to allow easy uninstallation. 70 | 71 | ``sudo -H pip3 install https://github.com/ssokolow/quicktile/archive/master.zip`` 72 | 73 | QuickTile's dependence on PyGObject prevents a fully PyPI-based installation 74 | option. 75 | 76 | Consult the `Installation `_ 77 | section of the manual for full details and alternative installation options. 78 | 79 | **First-Run Instructions for Global Hotkeys:** 80 | 81 | 1. Run ``quicktile`` or ``./quicktile.sh`` once to generate your configuration 82 | file at ``~/.config/quicktile.cfg``. 83 | 2. Edit the keybindings as desired. 84 | 3. Run ``quicktile --daemonize`` or ``./quicktile.sh --daemonize`` to bind to 85 | global hotkeys. 86 | 4. If everything seems to be working, add ``quicktile --daemonize`` or 87 | ``/full/path/to/quicktile.sh --daemonize`` to the list of commands your 88 | desktop will run on login. 89 | 90 | Consult the `Configuration `_ 91 | section of the manual for further details. 92 | 93 | Important Notes: 94 | ^^^^^^^^^^^^^^^^ 95 | 96 | * If run in a terminal, QuickTile's ``--daemonize`` option will attempt to 97 | report any problems with claiming global hotkeys for itself. 98 | * You can get a list of valid actions for the configuration file by running 99 | ``quicktile --show-actions``. 100 | * You can list your current keybindings by running 101 | ``quicktile --show-bindings``. 102 | * If you experience problems, please consult the `FAQ 103 | `_ section of the manual before 104 | reporting an issue. 105 | 106 | Usage (Typical) 107 | --------------- 108 | 109 | 1. Focus the window you want to tile 110 | 2. Hold the modifiers defined in ``ModMask`` (``Ctrl+Alt`` by default). 111 | 3. Repeatedly press one of the defined keybindings to cycle through window 112 | sizes available at the desired location on the screen. 113 | 114 | Consult ``quicktile --show-bindings`` or the `Command Reference 115 | `_ section of the manual for a list 116 | of default keybindings. 117 | 118 | (For example, under default settings, repeatedly pressing ``Ctrl+Alt+7`` will 119 | place the active window in the top-left corner of the screen and cycle it 120 | through different width presets.) 121 | 122 | This works best when combined with functionality your existing window manager 123 | provides (eg. ``Alt+Tab``) to minimize the need to switch your hand between your 124 | keyboard and your mouse. 125 | 126 | See the `Usage `_ section of the 127 | manual for alternative ways to interact with QuickTile. 128 | 129 | Removal 130 | ------- 131 | 132 | If you used the installation instructions listed above, a system-wide 133 | installation of QuickTile can be removed with the following commands: 134 | 135 | .. code:: sh 136 | 137 | sudo pip3 uninstall quicktile 138 | sudo rm /usr/local/bin/quicktile 139 | 140 | See the `Removal `_ 141 | section of the manual for instructions on clearing out files left behind by 142 | other installation methods. 143 | 144 | Contributing 145 | ------------ 146 | 147 | I welcome contributions. 148 | 149 | The recommended approach to make sure minimal effort is wasted is to open an 150 | issue indicating your interest in working on something. That way, I can let you 151 | know if there are any non-obvious design concerns that might hold up my 152 | accepting your pull requests. 153 | 154 | If you're looking for something to do, a ready supply 155 | of simple TODOs is split across two different mechanisms: 156 | 157 | 1. Run ``grep -R TODO *.py quicktile/`` in the project root. 158 | 2. Set ``todo_include_todos = True`` in ``docs/conf.py`` and run 159 | ``cd docs; make html`` to generate a version of the manual with a TODO 160 | listing on the top-level API documentation page. 161 | 162 | See the `Developer's Guide `_ 163 | for more information. 164 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | quicktile (0.4.0gtk3-1) UNRELEASED; urgency=medium 2 | 3 | * First Debian package. 4 | 5 | -- Stéphane Gourichon Wed, 01 Jan 2020 17:57:34 +0100 6 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: quicktile 2 | Maintainer: Stéphane Gourichon 3 | Section: misc 4 | Priority: optional 5 | Build-Depends: debhelper (>= 9), 6 | build-essential, 7 | devscripts, 8 | dh-python, 9 | fakeroot, 10 | python3, 11 | python3-pip, 12 | python3-setuptools, 13 | python3-gi, 14 | python3-xlib, 15 | python3-dbus, 16 | gir1.2-gtk-3.0, 17 | gir1.2-wnck-3.0, 18 | gir1.2-glib-2.0 19 | Standards-Version: 4.4.0 20 | 21 | Package: quicktile 22 | Architecture: all 23 | Depends: ${shlibs:Depends}, 24 | ${misc:Depends} 25 | Description: Keyboard-driven Window Tiling for your existing X11 window manager 26 | QuickTile is a simple utility, inspired by WinSplit Revolution for 27 | Windows, which adds window-tiling keybindings to your existing X11 28 | window manager. 29 | . 30 | You can also think of it as a standalone alternative to the keyboard- 31 | related features of the Compiz Grid plugin. 32 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | -------------------------------------------------------------------------------- /debian/docs: -------------------------------------------------------------------------------- 1 | quicktile 2 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | # See debhelper(7) (uncomment to enable) 3 | # output every command that modifies files on the build system. 4 | #DH_VERBOSE = 1 5 | 6 | # see EXAMPLES in dpkg-buildflags(1) and read /usr/share/dpkg/* 7 | DPKG_EXPORT_BUILDFLAGS = 1 8 | include /usr/share/dpkg/default.mk 9 | 10 | # see FEATURE AREAS in dpkg-buildflags(1) 11 | #export DEB_BUILD_MAINT_OPTIONS = hardening=+all 12 | 13 | # see ENVIRONMENT in dpkg-buildflags(1) 14 | # package maintainers to append CFLAGS 15 | #export DEB_CFLAGS_MAINT_APPEND = -Wall -pedantic 16 | # package maintainers to append LDFLAGS 17 | #export DEB_LDFLAGS_MAINT_APPEND = -Wl,--as-needed 18 | 19 | # main packaging script based on dh7 syntax 20 | %: 21 | dh $@ --with python3 --buildsystem=pybuild 22 | 23 | override_dh_auto_install: 24 | dh_auto_install 25 | install -D quicktile.desktop debian/quicktile/etc/xdg/autostart/quicktile.desktop 26 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /dev_requirements.txt: -------------------------------------------------------------------------------- 1 | flake8 2 | mypy 3 | nose 4 | coverage 5 | sphinx 6 | sphinx-autodoc-typehints[type_comment] 7 | sphinxcontrib-autoprogram 8 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean coverage html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | coverage: 53 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 54 | 55 | html: 56 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 57 | @echo 58 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 59 | 60 | dirhtml: 61 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 62 | @echo 63 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 64 | 65 | singlehtml: 66 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 67 | @echo 68 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 69 | 70 | pickle: 71 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 72 | @echo 73 | @echo "Build finished; now you can process the pickle files." 74 | 75 | json: 76 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 77 | @echo 78 | @echo "Build finished; now you can process the JSON files." 79 | 80 | htmlhelp: 81 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 82 | @echo 83 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 84 | ".hhp project file in $(BUILDDIR)/htmlhelp." 85 | 86 | qthelp: 87 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 88 | @echo 89 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 90 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 91 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/QuickTile.qhcp" 92 | @echo "To view the help file:" 93 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/QuickTile.qhc" 94 | 95 | devhelp: 96 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 97 | @echo 98 | @echo "Build finished." 99 | @echo "To view the help file:" 100 | @echo "# mkdir -p $$HOME/.local/share/devhelp/QuickTile" 101 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/QuickTile" 102 | @echo "# devhelp" 103 | 104 | epub: 105 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 106 | @echo 107 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 108 | 109 | latex: 110 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 111 | @echo 112 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 113 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 114 | "(use \`make latexpdf' here to do that automatically)." 115 | 116 | latexpdf: 117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 118 | @echo "Running LaTeX files through pdflatex..." 119 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 120 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 121 | 122 | latexpdfja: 123 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 124 | @echo "Running LaTeX files through platex and dvipdfmx..." 125 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 126 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 127 | 128 | text: 129 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 130 | @echo 131 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 132 | 133 | man: 134 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 135 | @echo 136 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 137 | 138 | texinfo: 139 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 140 | @echo 141 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 142 | @echo "Run \`make' in that directory to run these through makeinfo" \ 143 | "(use \`make info' here to do that automatically)." 144 | 145 | info: 146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 147 | @echo "Running Texinfo files through makeinfo..." 148 | make -C $(BUILDDIR)/texinfo info 149 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 150 | 151 | gettext: 152 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 153 | @echo 154 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 155 | 156 | changes: 157 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 158 | @echo 159 | @echo "The overview file is in $(BUILDDIR)/changes." 160 | 161 | linkcheck: 162 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 163 | @echo 164 | @echo "Link check complete; look for any errors in the above output " \ 165 | "or in $(BUILDDIR)/linkcheck/output.txt." 166 | 167 | doctest: 168 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 169 | @echo "Testing of doctests in the sources finished, look at the " \ 170 | "results in $(BUILDDIR)/doctest/output.txt." 171 | 172 | xml: 173 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 174 | @echo 175 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 176 | 177 | pseudoxml: 178 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 179 | @echo 180 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 181 | -------------------------------------------------------------------------------- /docs/_static/contrib_box/bitcoin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/_static/contrib_box/bitcoin.png -------------------------------------------------------------------------------- /docs/_static/contrib_box/bug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/_static/contrib_box/bug.png -------------------------------------------------------------------------------- /docs/_static/contrib_box/flattr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/_static/contrib_box/flattr.png -------------------------------------------------------------------------------- /docs/_static/contrib_box/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/_static/contrib_box/github.png -------------------------------------------------------------------------------- /docs/_static/contrib_box/paypal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/_static/contrib_box/paypal.png -------------------------------------------------------------------------------- /docs/_static/custom.css: -------------------------------------------------------------------------------- 1 | /* Disable CSSLint because it doesn't play nicely with CSS that's extending 2 | * an existing system of selectors that doesn't also follow its rules. */ 3 | /* csslint ignore:start */ 4 | 5 | /* Replicate the old QuickTile "page on background" aesthetic and ensure the 6 | * content area always extends to the bottom of the viewport */ 7 | body { 8 | background: #aac; 9 | display: flex; 10 | flex-direction: column; 11 | min-height: 100vh; 12 | } 13 | div.document, 14 | div.footer { 15 | background: white; 16 | margin-top: 0; 17 | padding: 30px 20px; 18 | } 19 | div.document { 20 | flex-grow: 1; 21 | } 22 | div.footer { 23 | margin-bottom: 0; 24 | } 25 | 26 | /* I think the default heading font sizes are too big */ 27 | div.body h1 { 28 | font-size: 230%; 29 | font-weight: bold; 30 | } 31 | 32 | div.body h2 { 33 | font-size: 150%; 34 | } 35 | div.body h3 { 36 | font-size: 130%; 37 | } 38 | div.body h4 { 39 | font-size: 110%; 40 | } 41 | div.body h5 { 42 | font-size: 110%; 43 | } 44 | 45 | /* I don't like what auto-hyphenation does to things */ 46 | div.body p, 47 | div.body dd, 48 | div.body li, 49 | div.body blockquote { 50 | -moz-hyphens: manual; 51 | -ms-hyphens: manual; 52 | -webkit-hyphens: manual; 53 | hyphens: manual; 54 | } 55 | 56 | /* Make GUI labels and Menu Selections stand out again after setting the 57 | * default font to sans-serif in conf.py. */ 58 | .guilabel, 59 | .menuselection { 60 | font-weight: bold; 61 | } 62 | 63 | /* Make code with line numbers consistent width */ 64 | table.highlighttable { 65 | width: 100%; 66 | } 67 | table.highlighttable td.linenos { 68 | width: 2em; 69 | } 70 | 71 | /* I don't want to wait for the newer Alabaster with nice markup */ 72 | kbd { 73 | border: 1px solid gray; 74 | border-radius: 0.5ex; 75 | font-size: 70%; 76 | padding: 0.3ex; 77 | box-shadow: inset 0 -1px 0 lightgray; 78 | vertical-align: top; 79 | } 80 | 81 | /* Centering helper for custom badges */ 82 | div.sphinxsidebarwrapper .badges { 83 | margin-top: 1em; 84 | margin-left: 3px; 85 | } 86 | div.sphinxsidebarwrapper .badges a { 87 | border-bottom: 0; 88 | } 89 | 90 | /* Adjust @media for Alabaster to match my changes */ 91 | @media screen and (max-width: 875px) { 92 | body { 93 | background: #fff; 94 | padding: 20px 30px; 95 | } 96 | div.document, 97 | div.footer { 98 | padding: 0; 99 | } 100 | div.sphinxsidebar .badges, 101 | div.sphinxsidebar .contrib_box { 102 | display: none; 103 | } 104 | } 105 | @media print { 106 | body { 107 | display: block; 108 | background: none; 109 | } 110 | img.animation { 111 | display: none; 112 | } 113 | } 114 | 115 | /* Workaround for bugs in Alabaster theme's mobile support */ 116 | @media screen and (max-width: 875px) { 117 | ul, 118 | ol { 119 | margin: 10px 0 10px 30px; 120 | } 121 | div.sphinxsidebar li, 122 | div.sphinxsidebar tt, 123 | div.sphinxsidebar code { 124 | color: #fff; 125 | } 126 | } 127 | @media print { 128 | img.github { 129 | display: none; 130 | } 131 | div.document { 132 | width: auto; 133 | } 134 | div.section { 135 | break-inside: avoid-page; 136 | } 137 | } 138 | 139 | /* Pure CSS "Fork me on GitHub" ribbon 140 | * Thanks to Chris Hellmann for the CSS I adapted to make this. 141 | * https://codepo8.github.io/css-fork-on-github-ribbon/ 142 | */ 143 | 144 | #forkongithub { 145 | position: absolute; 146 | display: block; 147 | top: 0; 148 | right: 0; 149 | width: 200px; 150 | overflow: hidden; 151 | height: 200px; 152 | z-index: 9999; 153 | } 154 | #forkongithub a { 155 | background: #000; 156 | color: #fff; 157 | text-decoration: none; 158 | font-family: arial, sans-serif; 159 | text-align: center; 160 | font-weight: bold; 161 | padding: 5px 40px; 162 | font-size: 10pt; 163 | line-height: 15pt; 164 | width: 200px; 165 | position: absolute; 166 | top: 40px; 167 | right: -82px; 168 | transform: rotate(45deg); 169 | -webkit-transform: rotate(45deg); 170 | -ms-transform: rotate(45deg); 171 | -moz-transform: rotate(45deg); 172 | -o-transform: rotate(45deg); 173 | box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); 174 | } 175 | #forkongithub a::before, 176 | #forkongithub a::after { 177 | content: ""; 178 | width: 100%; 179 | display: block; 180 | position: absolute; 181 | top: 1px; 182 | left: 0; 183 | height: 1px; 184 | background: #777; 185 | } 186 | #forkongithub a::after { 187 | bottom: 1px; 188 | top: auto; 189 | } 190 | @media screen and (max-width: 979px) { 191 | #forkongithub a { 192 | display: none; 193 | } 194 | } 195 | @media print { 196 | #forkongithub a { 197 | display: none; 198 | } 199 | } 200 | 201 | /* Button CSS borrowed from the old site */ 202 | div.sphinxsidebar a.btn { 203 | color: #3e4349; 204 | } 205 | .btn { 206 | display: inline-block; 207 | padding: 1px 2px; 208 | margin-bottom: 0; 209 | font-size: 8pt; 210 | font-weight: normal; 211 | line-height: 16px; 212 | color: #333333; 213 | text-align: center; 214 | text-decoration: none; 215 | text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); 216 | vertical-align: middle; 217 | cursor: pointer; 218 | background-color: #f5f5f5; 219 | background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6); 220 | background-image: -webkit-gradient( 221 | linear, 222 | 0 0, 223 | 0 100%, 224 | from(#ffffff), 225 | to(#e6e6e6) 226 | ); 227 | background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6); 228 | background-image: -o-linear-gradient(top, #ffffff, #e6e6e6); 229 | background-image: linear-gradient(to bottom, #ffffff, #e6e6e6); 230 | background-repeat: repeat-x; 231 | border: 1px solid #cccccc; 232 | border-color: #e6e6e6 #e6e6e6 #bfbfbf; 233 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 234 | border-bottom-color: #b3b3b3; 235 | -webkit-border-radius: 4px; 236 | -moz-border-radius: 4px; 237 | border-radius: 4px; 238 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 239 | 0 1px 2px rgba(0, 0, 0, 0.05); 240 | -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 241 | 0 1px 2px rgba(0, 0, 0, 0.05); 242 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 243 | 0 1px 2px rgba(0, 0, 0, 0.05); 244 | } 245 | 246 | .btn:hover, 247 | .btn:focus, 248 | .btn:active { 249 | color: #333333; 250 | background-color: #e6e6e6; 251 | } 252 | 253 | .btn:active { 254 | background-color: #cccccc; 255 | } 256 | 257 | .btn:hover, 258 | .btn:focus { 259 | color: #333333; 260 | text-decoration: none; 261 | background-position: 0 -15px; 262 | -webkit-transition: background-position 0.1s linear; 263 | -moz-transition: background-position 0.1s linear; 264 | -o-transition: background-position 0.1s linear; 265 | transition: background-position 0.1s linear; 266 | } 267 | 268 | .btn:focus { 269 | outline: thin dotted #333; 270 | outline: 5px auto -webkit-focus-ring-color; 271 | outline-offset: -2px; 272 | } 273 | .btn.active, 274 | .btn:active { 275 | background-image: none; 276 | outline: 0; 277 | -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 278 | 0 1px 2px rgba(0, 0, 0, 0.05); 279 | -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 280 | 0 1px 2px rgba(0, 0, 0, 0.05); 281 | box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); 282 | } 283 | 284 | .btn img { 285 | vertical-align: middle; 286 | } 287 | 288 | /* Contribution Box CSS from the old site */ 289 | .contrib_box { 290 | font-size: 80%; 291 | width: 10em; 292 | margin-top: 2em; 293 | padding: 5px 1ex 0; 294 | background: white; 295 | color: black; 296 | text-align: center; 297 | 298 | border: 1px solid rgb(187, 187, 187); 299 | box-shadow: 3px 4px 0px 0px rgb(221, 221, 221); 300 | -moz-box-shadow: 3px 4px 0px 0px rgb(221, 221, 221); 301 | -webkit-box-shadow: 3px 4px 0px 0px rgb(221, 221, 221); 302 | } 303 | 304 | div.contrib_box h3 { 305 | font-size: 125%; 306 | font-weight: bold; 307 | } 308 | 309 | div.contrib_box dt { 310 | font-weight: bold; 311 | } 312 | div.contrib_box dd { 313 | margin: 2px 0; 314 | } 315 | 316 | div.sphinxsidebar .contrib_box a { 317 | border-bottom: 0; 318 | } 319 | div.sphinxsidebar .contrib_box input { 320 | border: 0; 321 | } 322 | div.sphinxsidebar .contrib_box ul { 323 | margin: 4px auto; 324 | } 325 | div.sphinxsidebar .contrib_box ul.icons { 326 | padding: 0; 327 | text-align: center; 328 | list-style-type: none; 329 | } 330 | .contrib_box ul.icons li, 331 | ul.icons form { 332 | display: inline; 333 | } 334 | /* csslint ignore:end */ 335 | -------------------------------------------------------------------------------- /docs/_static/img/debian_12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/_static/img/debian_12.png -------------------------------------------------------------------------------- /docs/_static/img/mint_12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/_static/img/mint_12.png -------------------------------------------------------------------------------- /docs/_static/img/ubuntu_12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/_static/img/ubuntu_12.png -------------------------------------------------------------------------------- /docs/_static/license.svg: -------------------------------------------------------------------------------- 1 | LicenseLicenseGPLv2+GPLv2+ -------------------------------------------------------------------------------- /docs/_templates/about.html: -------------------------------------------------------------------------------- 1 | {# about.html from Alabaster with fixed alt text for badges #} 2 | {% if theme_logo %} 3 |

{{ project }}

8 | {% endif %} 9 | 10 |

11 | {% else %} 12 |

{{ project }}

13 | {% endif %} 14 | 15 | {% if theme_description %} 16 |

{{ theme_description }}

17 | {% endif %} 18 | 19 | {% if theme_github_user and theme_github_repo %} 20 | {% if theme_github_button|lower == 'true' %} 21 |

22 | 24 |

25 | {% endif %} 26 | {% endif %} 27 | 28 | {% if theme_travis_button|lower != 'false' %} 29 | {% if theme_travis_button|lower == 'true' %} 30 | {% set path = theme_github_user + '/' + theme_github_repo %} 31 | {% else %} 32 | {% set path = theme_travis_button %} 33 | {% endif %} 34 |

35 | 36 | CI Test Status 40 | 41 |

42 | {% endif %} 43 | 44 | {% if theme_codecov_button|lower != 'false' %} 45 | {% if theme_codecov_button|lower == 'true' %} 46 | {% set path = theme_github_user + '/' + theme_github_repo %} 47 | {% else %} 48 | {% set path = theme_codecov_button %} 49 | {% endif %} 50 |

51 | 52 | Code Coverage 56 | 57 |

58 | {% endif %} 59 | -------------------------------------------------------------------------------- /docs/_templates/donate.html: -------------------------------------------------------------------------------- 1 |
2 |

Tip Jar

3 |
4 |
Code (Best)
5 |
6 |
    7 |
  • GitHub
  • 11 |
  • Bug Tracker
  • 15 |
16 |
17 |
Publicity (Better)
18 |
Show your friends
19 |
Snacks (Good)
20 |
21 |
    22 |
  • Flattr
  • 26 |
  • 27 |
  • BitCoin
  • 33 |
34 |
35 |
36 |
37 | 42 | -------------------------------------------------------------------------------- /docs/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends '!layout.html' %} 2 | {%- block extrahead %} 3 | 5 | {{ super() }} 6 | {% endblock %} 7 | {%- block footer %} 8 | {{super()}} 9 | Fork me on GitHub 10 | {%- endblock %} 11 | -------------------------------------------------------------------------------- /docs/_templates/sidebar_badges.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /docs/animation/.gitignore: -------------------------------------------------------------------------------- 1 | /png 2 | -------------------------------------------------------------------------------- /docs/animation/Makefile: -------------------------------------------------------------------------------- 1 | svgs := $(wildcard svg/*.svg) 2 | pngs := $(patsubst svg/%.svg,png/%.png,$(svgs)) 3 | 4 | .PHONY: all 5 | 6 | all: animation.gif 7 | 8 | animation.gif: $(pngs) 9 | # Ensure the order is correct in Geeqie's "by date" sorting 10 | find png/ -iname '*.png' | sort | xargs touch 11 | 12 | # Convert with alpha removal to avoid jaggies on the curves 13 | convert -delay 100 png/*.png -alpha remove -alpha off animation.gif 14 | gifsicle --batch -O3 animation.gif 15 | 16 | png/%.png: svg/%.svg | png 17 | inkscape $< --export-png=$@ 18 | 19 | png: 20 | mkdir -p $@ 21 | 22 | .PHONY: clean 23 | clean: 24 | rm -rf png 25 | rm animation.gif 26 | -------------------------------------------------------------------------------- /docs/animation/animation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/animation/animation.gif -------------------------------------------------------------------------------- /docs/apidocs/__main__.rst: -------------------------------------------------------------------------------- 1 | Core Application Code (``__main__.py``) 2 | ======================================= 3 | 4 | .. automodule:: quicktile.__main__ 5 | :members: 6 | 7 | ---- 8 | 9 | Thanks to Thomas Vander Stichele for some of the documentation cleanups during 10 | the GTK+ 2.x era. 11 | -------------------------------------------------------------------------------- /docs/apidocs/commands.rst: -------------------------------------------------------------------------------- 1 | Command Registry and Included Commands (``commands.py``) 2 | ======================================================== 3 | 4 | .. automodule:: quicktile.commands 5 | :members: 6 | :exclude-members: CommandCBWrapper 7 | -------------------------------------------------------------------------------- /docs/apidocs/config.rst: -------------------------------------------------------------------------------- 1 | Configuration Handling (``config.py``) 2 | ====================================== 3 | 4 | .. automodule:: quicktile.config 5 | :members: 6 | :exclude-members: XDG_CONFIG_DIR,DEFAULTS,CfgDict 7 | 8 | ---- 9 | 10 | .. pprint:: quicktile.config.DEFAULTS 11 | -------------------------------------------------------------------------------- /docs/apidocs/dbus_api.rst: -------------------------------------------------------------------------------- 1 | D-Bus Interface (``dbus_api.py``) 2 | ================================= 3 | 4 | .. automodule:: quicktile.dbus_api 5 | :members: 6 | :exclude-members: _dbus_class_table 7 | -------------------------------------------------------------------------------- /docs/apidocs/functional_harness/env_general.rst: -------------------------------------------------------------------------------- 1 | Environment Management (``functional_harness/env_general.py``) 2 | ============================================================== 3 | 4 | .. automodule:: functional_harness.env_general 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/apidocs/functional_harness/x_server.rst: -------------------------------------------------------------------------------- 1 | X Server Management (``functional_harness/x_server.py``) 2 | ======================================================== 3 | 4 | .. automodule:: functional_harness.x_server 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/apidocs/gtkexcepthook.rst: -------------------------------------------------------------------------------- 1 | GUI Exception Handler (``gtkexcepthook.py``) 2 | ============================================ 3 | 4 | .. automodule:: quicktile.gtkexcepthook 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/apidocs/index.rst: -------------------------------------------------------------------------------- 1 | API Documentation 2 | ================= 3 | 4 | QuickTile's code is currently divided into the following modules: 5 | 6 | .. toctree:: 7 | :maxdepth: 1 8 | 9 | __main__ 10 | commands 11 | config 12 | dbus_api 13 | gtkexcepthook 14 | keybinder 15 | layout 16 | util 17 | wm 18 | tests 19 | 20 | .. ifconfig:: todo_include_todos 21 | 22 | TODO Annotations Overview 23 | ========================= 24 | 25 | The following is a complete list of :rst:dir:`todo` annotations found 26 | in the source code. 27 | 28 | .. todolist:: 29 | -------------------------------------------------------------------------------- /docs/apidocs/keybinder.rst: -------------------------------------------------------------------------------- 1 | Internal Global Hotkey Support (``keybinder.py``) 2 | ================================================= 3 | 4 | .. automodule:: quicktile.keybinder 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/apidocs/layout.rst: -------------------------------------------------------------------------------- 1 | Layout Calculations (``layout.py``) 2 | =================================== 3 | 4 | .. automodule:: quicktile.layout 5 | :members: 6 | :exclude-members: Geom 7 | -------------------------------------------------------------------------------- /docs/apidocs/test_functional.rst: -------------------------------------------------------------------------------- 1 | Functional Tests (``test_functional.py``) 2 | ========================================= 3 | 4 | .. automodule:: test_functional 5 | :members: 6 | :exclude-members: TEST_SCRIPT 7 | 8 | ---- 9 | 10 | Pending proper tests and assertions, the following sequence of commands is 11 | executed as an attempt to elicit uncaught exceptions. 12 | 13 | .. pprint:: test_functional.TEST_SCRIPT 14 | -------------------------------------------------------------------------------- /docs/apidocs/test_util.rst: -------------------------------------------------------------------------------- 1 | Unit Tests for Utility Functions (``test_util.py``) 2 | =================================================== 3 | 4 | .. automodule:: tests.test_util 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/apidocs/tests.rst: -------------------------------------------------------------------------------- 1 | Automated Testing Code 2 | ======================= 3 | 4 | QuickTile currently has a nearly complete unit test suite for the contents 5 | of :mod:`quicktile.util` as well as the beginnings of a functional testing 6 | harness which doesn't yet do much, and a scattering of doctests throughout the 7 | rest of the code. 8 | 9 | .. toctree:: 10 | :maxdepth: 1 11 | 12 | test_util 13 | test_functional 14 | functional_harness/x_server 15 | functional_harness/env_general 16 | -------------------------------------------------------------------------------- /docs/apidocs/util.rst: -------------------------------------------------------------------------------- 1 | Self-Contained Utility Code (``util.py``) 2 | ========================================= 3 | 4 | .. automodule:: quicktile.util 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/apidocs/wm.rst: -------------------------------------------------------------------------------- 1 | Window Manager API Wrapper (``wm.py``) 2 | ====================================== 3 | 4 | .. automodule:: quicktile.wm 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/authors/index.rst: -------------------------------------------------------------------------------- 1 | .. 2 | IMPORTANT: Sphinx's text output silently erases hyperlinks without 3 | presenting the URLs in another fashion and renders inline images in a 4 | somewhat ugly style. Please bear this in mind when editing. 5 | 6 | Authors 7 | ======= 8 | 9 | .. only:: text 10 | 11 | IMPORTANT: THIS DOCUMENT IS AUTOGENERATED. To update it, modify 12 | docs/authors/index.rst in the QuickTile source distribution and run 13 | docs/update_authors.sh 14 | 15 | NOTE: Due to limitations in Sphinx's generation of text output, attribution 16 | URLs are missing from this version and the textual rendering of inline 17 | images is less than ideal. The HTML version included in the QuickTile 18 | manual should be preferred where possible. 19 | 20 | The Program 21 | ----------- 22 | 23 | QuickTile is primarily the work of `Stephan Sokolow`_, however, it has received 24 | various contributions over the years. 25 | 26 | Thanks go out to the following people: 27 | 28 | `David Stygstra`_ 29 | Generalized the ``move-to-center`` command into the ``move-to-*`` family 30 | of commands. 31 | 32 | `Fábio C. Barrionuevo da Luz`_ 33 | Fixed some permissioning warts in the install procedure. 34 | 35 | `Fritz Reichwald`_ 36 | Added ``Gtk.init_check`` in ``quicktile.__main__`` to work around a 37 | bug in some builds of GTK as well as a ``gi.require_version`` that 38 | is only necessary on some systems. 39 | 40 | `Gui Ambros`_ 41 | Identified the solution for a misalignment that occurs on HiDPI screens. 42 | 43 | `Gustavo J A M Carneiro`_ and `Filip Van Raemdonck`_ 44 | Created the ``gtkexcepthook`` module in `the form I started from 45 | `_. 46 | 47 | Justin 48 | Added the ``horizontal-maximize``, ``vertical-maximize``, and 49 | ``move-to-center`` commands. 50 | 51 | `Matthias Putz`_ 52 | Fixed use of an uninitialized variable in a difficult-to-trigger failure 53 | case. 54 | 55 | `Max Weiß`_ 56 | 57 | * Added the ``KEYLOOKUP`` dict to compensate for un-intuitive omissions for 58 | the symbol names of common keys. 59 | * Reworked the shebang to address the period when some Linux distros lacked 60 | a :command:`python2` binary while others linked :command:`python` to 61 | Python 3. 62 | 63 | `Oliver Gerlich`_ 64 | Corrected an omission on the list of dependencies for CentOS 7 and possibly 65 | other Red Hat-family distros. 66 | 67 | `Stéphane Gourichon`_ 68 | 69 | * Researched how to create ``.deb`` packages 70 | * Wrote :file:`recompile_local_debian_package.sh` 71 | * Set up the necessary package metadata for building a ``.deb``. 72 | 73 | `Stuart Axelbrooke`_ 74 | Corrected a flaw in the window-tiling heuristics which prevented them 75 | from functioning correctly on especially large monitors. 76 | 77 | `Thomas Vander Stichele`_ 78 | Helped to clean up the API documentation during the ePyDoc_ era. 79 | 80 | `Valdis Vitolins`_ 81 | Added an omitted dependency on ``python-setuptools`` to :file:`README.rst` 82 | 83 | `Valentin Agachi`_ 84 | Corrected window-layout calculations to use :py:func:`round` for pixel 85 | values. 86 | 87 | `Yuting/Tim Xiao`_ 88 | Made the window-tiling heuristics more robust. 89 | 90 | 91 | The Manual 92 | ---------- 93 | 94 | With the following exceptions, all text and illustrations in the manual are 95 | copyright `Stephan Sokolow`_. 96 | 97 | It should be assumed that, as the contents of the API documentation section are 98 | extracted from the source code, the copyright of a given entry will be the same 99 | as the code it describes. 100 | 101 | The text of the Alabaster_ theme for Sphinx_, and any generated text inserted 102 | by Sphinx are copyright their respective rightsholders. 103 | 104 | Imagery 105 | ^^^^^^^ 106 | 107 | The following illustrations are copyright `David Stygstra`_: 108 | 109 | * ``bordered.svg`` 110 | * ``bottom-left.svg`` 111 | * ``bottom-right.svg`` 112 | * ``bottom.svg`` 113 | * ``fullscreen.svg`` 114 | * ``horizontal-maximize.svg`` 115 | * ``left.svg`` 116 | * ``maximize.svg`` 117 | * ``middle.svg`` 118 | * ``minimize.svg`` 119 | * ``move-to-bottom-left.svg`` 120 | * ``move-to-bottom-right.svg`` 121 | * ``move-to-bottom.svg`` 122 | * ``move-to-center.svg`` 123 | * ``move-to-left.svg`` 124 | * ``move-to-right.svg`` 125 | * ``move-to-top-left.svg`` 126 | * ``move-to-top-right.svg`` 127 | * ``move-to-top.svg`` 128 | * ``right.svg`` 129 | * ``shade.svg`` 130 | * ``top-left.svg`` 131 | * ``top-right.svg`` 132 | * ``top.svg`` 133 | * ``vertical-maximize.svg`` 134 | 135 | |bug.png| and |wrench.png| from the `Silk Icons`_ set by `Mark James`_ 136 | are used under the `Creative Commons Attribution 2.5`_ license. 137 | 138 | 139 | The |license.svg| badge is a locally cached copy of an SVG file generated by 140 | Shields.io_. 141 | 142 | All other favicons and logos are copyright their respective owners and used 143 | only to display favicon-style links to their owners' websites. 144 | 145 | .. |bug.png| image:: ../_static/contrib_box/bug.png 146 | .. |wrench.png| image:: ../wrench.png 147 | 148 | .. |license.svg| image:: ../_static/license.svg 149 | :alt: GPLv2+ License 150 | 151 | .. _Alabaster: https://alabaster.readthedocs.io/ 152 | .. _Creative Commons Attribution 2.5: https://creativecommons.org/licenses/by/2.5/ 153 | .. _David Stygstra: https://github.com/stygstra 154 | .. _ePyDoc: http://epydoc.sourceforge.net/ 155 | .. _Fábio C. Barrionuevo da Luz: https://github.com/luzfcb 156 | .. _Filip van Raemdonck: https://www.linkedin.com/in/filip-van-raemdonck/ 157 | .. _Fritz Reichwald: https://github.com/fiete201 158 | .. _Gui Ambros: https://github.com/guiambros 159 | .. _Gustavo J A M Carneiro: https://github.com/gjcarneiro 160 | .. _Mark James: https://twitter.com/markjames 161 | .. _Matthias Putz: https://github.com/mputz86 162 | .. _Max Weiß: https://github.com/wmax 163 | .. _Oliver Gerlich: https://github.com/oliver 164 | .. _Shields.io: https://shields.io/ 165 | .. _Silk Icons: http://www.famfamfam.com/lab/icons/silk/ 166 | .. _Sphinx: https://alabaster.readthedocs.io/ 167 | .. _Stephan Sokolow: http://ssokolow.com/ 168 | .. _Stéphane Gourichon: https://github.com/fidergo-stephane-gourichon 169 | .. _Stuart Axelbrooke: https://github.com/soaxelbrooke 170 | .. _Thomas Vander Stichele: https://thomas.apestaart.org/ 171 | .. _Valdis Vitolins: https://github.com/valdisvi 172 | .. _Valentin Agachi: https://github.com/avaly 173 | .. _Yuting/Tim Xiao: https://github.com/txiao 174 | 175 | .. 176 | NOTE: For "Yuting/Tim Xiao", the commits are signed "Yuting Xiao" but the 177 | name on the associated GitHub account has since been changed to "Tim Xiao". 178 | I am operating on the assumption that this is a case of "Yuting" being the 179 | contributor's legal name and "Tim" being a nickname the contributor has 180 | taken to more easily interact with peers... it's apparently a common 181 | practice. 182 | -------------------------------------------------------------------------------- /docs/cli.rst: -------------------------------------------------------------------------------- 1 | Command-Line Arguments 2 | ====================== 3 | 4 | .. autoprogram:: quicktile.__main__:argparser() 5 | :prog: quicktile 6 | :groups: 7 | -------------------------------------------------------------------------------- /docs/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd "$(dirname "$(readlink -f "$0")")" || exit 1 3 | 4 | # Set up deploy key 5 | openssl aes-256-cbc -K "$encrypted_0e452a363468_key" -iv "$encrypted_0e452a363468_iv" -in publish-key.enc -out ~/.ssh/publish-key -d || exit 2 6 | chmod u=rw,og= ~/.ssh/publish-key || exit 3 7 | echo "Host github.com" >> ~/.ssh/config || exit 4 8 | echo " IdentityFile ~/.ssh/publish-key" >> ~/.ssh/config || exit 5 9 | 10 | # Set up gh-pages remote 11 | git --version || exit 6 12 | git remote set-url origin git@github.com:ssokolow/quicktile.git || exit 7 13 | git fetch origin -f gh-pages:gh-pages || exit 8 14 | 15 | # Build docs 16 | pip3 install -r ../dev_requirements.txt || exit 9 17 | make html || exit 10 18 | 19 | # Overwrite gh-pages with updated docs 20 | ghp-import -n -p -m "Update gh-pages." _build/html || exit 11 21 | -------------------------------------------------------------------------------- /docs/diagrams/.gitignore: -------------------------------------------------------------------------------- 1 | .*.sw* 2 | .sw* 3 | -------------------------------------------------------------------------------- /docs/diagrams/Makefile: -------------------------------------------------------------------------------- 1 | svgs := $(wildcard svg/*.svg) 2 | pngs := $(patsubst svg/%.svg,png/%.png,$(svgs)) 3 | 4 | .PHONY: all 5 | all: $(pngs) 6 | 7 | png/%.png: svg/%.svg | png 8 | inkscape $< --export-png=$@ 9 | optipng $@ 10 | advpng -z4 $@ 11 | 12 | png: 13 | mkdir -p $@ 14 | 15 | .PHONY: clean 16 | clean: 17 | rm -rf png 18 | -------------------------------------------------------------------------------- /docs/diagrams/README.md: -------------------------------------------------------------------------------- 1 | Diagrams for [QuickTile](https://github.com/ssokolow/quicktile) commands. 2 | 3 | Generate PNGs by running `make`. Requires [Inkscape](https://inkscape.org/), 4 | [OptiPNG](http://optipng.sourceforge.net/), and 5 | [AdvanceCOMP](http://www.advancemame.it/comp-readme). 6 | -------------------------------------------------------------------------------- /docs/diagrams/png/all-desktops.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/all-desktops.png -------------------------------------------------------------------------------- /docs/diagrams/png/always-above.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/always-above.png -------------------------------------------------------------------------------- /docs/diagrams/png/always-below.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/always-below.png -------------------------------------------------------------------------------- /docs/diagrams/png/bordered.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/bordered.png -------------------------------------------------------------------------------- /docs/diagrams/png/bottom-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/bottom-left.png -------------------------------------------------------------------------------- /docs/diagrams/png/bottom-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/bottom-right.png -------------------------------------------------------------------------------- /docs/diagrams/png/bottom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/bottom.png -------------------------------------------------------------------------------- /docs/diagrams/png/center.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/center.png -------------------------------------------------------------------------------- /docs/diagrams/png/fullscreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/fullscreen.png -------------------------------------------------------------------------------- /docs/diagrams/png/horizontal-maximize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/horizontal-maximize.png -------------------------------------------------------------------------------- /docs/diagrams/png/left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/left.png -------------------------------------------------------------------------------- /docs/diagrams/png/maximize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/maximize.png -------------------------------------------------------------------------------- /docs/diagrams/png/minimize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/minimize.png -------------------------------------------------------------------------------- /docs/diagrams/png/monitor-next-all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/monitor-next-all.png -------------------------------------------------------------------------------- /docs/diagrams/png/monitor-next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/monitor-next.png -------------------------------------------------------------------------------- /docs/diagrams/png/monitor-prev-all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/monitor-prev-all.png -------------------------------------------------------------------------------- /docs/diagrams/png/monitor-prev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/monitor-prev.png -------------------------------------------------------------------------------- /docs/diagrams/png/move-to-bottom-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/move-to-bottom-left.png -------------------------------------------------------------------------------- /docs/diagrams/png/move-to-bottom-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/move-to-bottom-right.png -------------------------------------------------------------------------------- /docs/diagrams/png/move-to-bottom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/move-to-bottom.png -------------------------------------------------------------------------------- /docs/diagrams/png/move-to-center.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/move-to-center.png -------------------------------------------------------------------------------- /docs/diagrams/png/move-to-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/move-to-left.png -------------------------------------------------------------------------------- /docs/diagrams/png/move-to-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/move-to-right.png -------------------------------------------------------------------------------- /docs/diagrams/png/move-to-top-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/move-to-top-left.png -------------------------------------------------------------------------------- /docs/diagrams/png/move-to-top-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/move-to-top-right.png -------------------------------------------------------------------------------- /docs/diagrams/png/move-to-top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/move-to-top.png -------------------------------------------------------------------------------- /docs/diagrams/png/right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/right.png -------------------------------------------------------------------------------- /docs/diagrams/png/shade.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/shade.png -------------------------------------------------------------------------------- /docs/diagrams/png/show-desktop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/show-desktop.png -------------------------------------------------------------------------------- /docs/diagrams/png/top-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/top-left.png -------------------------------------------------------------------------------- /docs/diagrams/png/top-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/top-right.png -------------------------------------------------------------------------------- /docs/diagrams/png/top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/top.png -------------------------------------------------------------------------------- /docs/diagrams/png/trigger-move.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/trigger-move.png -------------------------------------------------------------------------------- /docs/diagrams/png/trigger-resize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/trigger-resize.png -------------------------------------------------------------------------------- /docs/diagrams/png/vertical-maximize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/vertical-maximize.png -------------------------------------------------------------------------------- /docs/diagrams/png/workspace-go-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/workspace-go-down.png -------------------------------------------------------------------------------- /docs/diagrams/png/workspace-go-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/workspace-go-left.png -------------------------------------------------------------------------------- /docs/diagrams/png/workspace-go-next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/workspace-go-next.png -------------------------------------------------------------------------------- /docs/diagrams/png/workspace-go-prev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/workspace-go-prev.png -------------------------------------------------------------------------------- /docs/diagrams/png/workspace-go-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/workspace-go-right.png -------------------------------------------------------------------------------- /docs/diagrams/png/workspace-go-up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/workspace-go-up.png -------------------------------------------------------------------------------- /docs/diagrams/png/workspace-send-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/workspace-send-down.png -------------------------------------------------------------------------------- /docs/diagrams/png/workspace-send-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/workspace-send-left.png -------------------------------------------------------------------------------- /docs/diagrams/png/workspace-send-next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/workspace-send-next.png -------------------------------------------------------------------------------- /docs/diagrams/png/workspace-send-prev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/workspace-send-prev.png -------------------------------------------------------------------------------- /docs/diagrams/png/workspace-send-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/workspace-send-right.png -------------------------------------------------------------------------------- /docs/diagrams/png/workspace-send-up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/diagrams/png/workspace-send-up.png -------------------------------------------------------------------------------- /docs/diagrams/svg/minimize.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 45 | 47 | 48 | 50 | image/svg+xml 51 | 53 | 54 | 55 | 56 | 57 | 62 | 69 | 76 | 83 | 90 | 96 | 103 | 110 | 117 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /docs/diagrams/svg/move-to-bottom-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 45 | 47 | 48 | 50 | image/svg+xml 51 | 53 | 54 | 55 | 56 | 57 | 62 | 69 | 76 | 83 | 90 | 97 | 104 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /docs/diagrams/svg/move-to-bottom-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 45 | 47 | 48 | 50 | image/svg+xml 51 | 53 | 54 | 55 | 56 | 57 | 62 | 69 | 76 | 83 | 90 | 97 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /docs/diagrams/svg/move-to-bottom.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 45 | 47 | 48 | 50 | image/svg+xml 51 | 53 | 54 | 55 | 56 | 57 | 62 | 69 | 76 | 83 | 90 | 97 | 104 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /docs/diagrams/svg/move-to-center.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 45 | 47 | 48 | 50 | image/svg+xml 51 | 53 | 54 | 55 | 56 | 57 | 62 | 69 | 76 | 83 | 90 | 97 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /docs/diagrams/svg/move-to-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 45 | 47 | 48 | 50 | image/svg+xml 51 | 53 | 54 | 55 | 56 | 57 | 62 | 69 | 76 | 83 | 89 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /docs/diagrams/svg/move-to-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 45 | 47 | 48 | 50 | image/svg+xml 51 | 53 | 54 | 55 | 56 | 57 | 62 | 69 | 76 | 83 | 89 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /docs/diagrams/svg/move-to-top-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 45 | 47 | 48 | 50 | image/svg+xml 51 | 53 | 54 | 55 | 56 | 57 | 62 | 69 | 76 | 83 | 90 | 97 | 104 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /docs/diagrams/svg/move-to-top-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 45 | 47 | 48 | 50 | image/svg+xml 51 | 53 | 54 | 55 | 56 | 57 | 62 | 69 | 76 | 83 | 90 | 97 | 104 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /docs/diagrams/svg/move-to-top.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 45 | 47 | 48 | 50 | image/svg+xml 51 | 53 | 54 | 55 | 56 | 57 | 62 | 69 | 76 | 83 | 90 | 97 | 104 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /docs/diagrams/svg/shade.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 45 | 47 | 48 | 50 | image/svg+xml 51 | 53 | 54 | 55 | 56 | 57 | 62 | 69 | 76 | 83 | 89 | 95 | 101 | 107 | 113 | 119 | 1 130 | 2 141 | 148 | 155 | 3 166 | 173 | 180 | 187 | 188 | 189 | -------------------------------------------------------------------------------- /docs/diagrams/svg/trigger-resize.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 45 | 47 | 48 | 50 | image/svg+xml 51 | 53 | 54 | 55 | 56 | 57 | 62 | 69 | 76 | 82 | 89 | 96 | 99 | 105 | 111 | 112 | 115 | 122 | 129 | 136 | 143 | 149 | 155 | 161 | 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /docs/diagrams/svg/vertical-maximize.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 45 | 47 | 48 | 50 | image/svg+xml 51 | 53 | 54 | 55 | 56 | 57 | 62 | 68 | 75 | 82 | 89 | 96 | 102 | 108 | 114 | 120 | 126 | 1 137 | 2 148 | 154 | 160 | 167 | 174 | 3 185 | 186 | 187 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | QuickTile 2 | ========= 3 | 4 | .. image:: animation/animation.gif 5 | :class: animation 6 | :alt: demonstration animation 7 | 8 | QuickTile is a simple utility, inspired by WinSplit Revolution for Windows, 9 | which adds window-tiling keybindings to your existing X11 window manager. 10 | 11 | You can also think of it as a standalone alternative to the keyboard-related 12 | features of the Compiz Grid plugin. 13 | 14 | QuickTile adds no new visible UI elements. No tray icon that just wastes space 15 | once you're done configuring it. Once it's set up, you've got your choice of 16 | new global hotkeys, a new D-Bus API, and/or a new command to invoke from shell 17 | scripts. That's it. 18 | 19 | For more details, see the illustrations in the :doc:`commands`. 20 | 21 | QuickTile depends on the Python bindings for libraries you :ref:`probably 22 | already have ` and can be run :ref:`without installing 23 | ` to easily try it out. 24 | 25 | Manual Contents: 26 | 27 | .. toctree:: 28 | :maxdepth: 1 29 | 30 | installation 31 | usage 32 | commands 33 | config 34 | cli 35 | faq 36 | apidocs/index 37 | developing 38 | authors/index 39 | 40 | * :ref:`modindex` 41 | * :ref:`genindex` 42 | 43 | .. |wrench| image:: wrench.png 44 | 45 | |wrench| Please report any issues on GitHub at 46 | `ssokolow/quicktile `_ 47 | 48 | .. 49 | bug.png and wrench.png from the Silk Icons set by Mark James, available at 50 | http://www.famfamfam.com/lab/icons/silk/ 51 | are used under the Creative Commons Attribution 2.5 license. 52 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\QuickTile.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\QuickTile.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /docs/publish-key.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/publish-key.enc -------------------------------------------------------------------------------- /docs/update_authors.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TMP_TARGET="_build/authors" 4 | 5 | cd "$(dirname "$(readlink -f "$0")")" 6 | 7 | rm -rf "$TMP_TARGET/index.txt" 8 | sphinx-build -b text -c . authors "$TMP_TARGET" 9 | mv $TMP_TARGET/index.txt ../AUTHORS 10 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | Usage 2 | ===== 3 | 4 | QuickTile is built around a simple model of applying :doc:`tiling commands 5 | ` to either the active window or, for some commands, the 6 | desktop as a whole. 7 | 8 | .. todo:: I'm not satisfied with the style I've written :doc:`usage` in. I'll 9 | need to come back to it later and try to puzzle out why. 10 | 11 | These commands can be invoked in one of three ways: 12 | 13 | .. contents:: 14 | :local: 15 | 16 | Global Hotkeys 17 | -------------- 18 | 19 | If QuickTile is started with the 20 | `-\\-daemonize `_ option, it will 21 | attempt to bind global hotkeys as defined by the mappings in 22 | :doc:`quicktile.cfg `. 23 | 24 | A typical use of QuickTile's hotkeys is as follows: 25 | 26 | 1. Focus the window you want to tile 27 | 2. Hold the modifiers defined in :ref:`ModMask ` (:kbd:`Ctrl` + 28 | :kbd:`Alt` by default). 29 | 3. Repeatedly press one of the defined keybindings to cycle through window 30 | sizes available at the desired location on the screen. 31 | 32 | This works best when combined with functionality your existing window manager 33 | provides (eg. :kbd:`Alt` + :kbd:`Tab`) to minimize the need to switch your hand between your 34 | keyboard and your mouse. 35 | 36 | See the :doc:`commands` section for a listing of default keybindings and what 37 | they do, or run ``quicktile --show-bindings`` and ``quicktile --show-actions``. 38 | 39 | Command-Line Invocation 40 | ----------------------- 41 | 42 | If QuickTile is started without ``--daemonize`` but with one or more positional 43 | arguments, it will perform the specified sequence of actions on the active 44 | window (or, depending on the command, on the desktop as a whole) and then exit. 45 | 46 | .. code-block:: shell-session 47 | 48 | $ quicktile top-left top-left 49 | 50 | This is useful for invoking QuickTile from incorporating it into shell scripts 51 | or binding tiling commands to things `XGrabKey`_ can't see, such as 52 | LIRC_-based remote controls via :manpage:`irexec(1)`. 53 | 54 | If running this in a context where it is undesirable for your script to block 55 | and display an error dialog on encountering an exception within QuickTile, 56 | please pass `-\\-no-excepthook `_ 57 | when invoking QuickTile. 58 | 59 | For more details on QuickTile's command-line interface, run ``quicktile 60 | --help`` or see the :doc:`cli` section of this manual. 61 | 62 | .. note:: Historically, most of the attention paid to QuickTile has been to 63 | its function under the influence of 64 | `-\\-daemonize `_ and command-line 65 | invocation has known bugs in how it interacts with the X server. 66 | 67 | Most notably, when multiple commands are specified on a single 68 | command-line, it triggers a race condition where QuickTile doesn't properly 69 | wait for a command's effects to take hold before the next command begins 70 | querying window shapes. 71 | 72 | The simplest demonstration of this on a mulit-monitor system is 73 | ``quicktile monitor-next top-left`` which will cause the ``top-left`` to 74 | reverse the effect of the ``monitor-next``. 75 | 76 | A fix for this is intended, but the non-trivial re-architecting involved 77 | means that I don't want to do until after the automated test suite is 78 | sufficiently complete. 79 | 80 | .. todo:: Fix the race conditions which prevent non-resident operation from 81 | functioning as expected. 82 | 83 | .. _LIRC: http://lirc.org/ 84 | .. _XGrabKey: https://tronche.com/gui/x/xlib/input/XGrabKey.html 85 | 86 | D-Bus API 87 | --------- 88 | 89 | Command-line invocation is useful but it *does* have a tendency to induce 90 | a perceptible delay between pressing a key/button and having the window 91 | respond. 92 | 93 | If `dbus-python `_ is installed, the 94 | `-\\-daemonize `_ command-line option will also 95 | attempt to claim the ``com.ssokolow.QuickTile`` service name. 96 | 97 | It will expose a single object path (``/com/ssokolow/QuickTile``) with a single 98 | interface (``com.ssokolow.QuickTile``) containing a single method 99 | (``doCommand``) which can be used to call tiling commands as if invoked 100 | by the global keybinding code. 101 | 102 | A good way to test this out is using Qt's :command:`qdbus` command, which 103 | serves as both a command-line D-Bus explorer and a client for calling D-Bus 104 | methods. 105 | 106 | .. code-block:: shell-session 107 | 108 | $ qdbus com.ssokolow.QuickTile 109 | / 110 | /com 111 | /com/ssokolow 112 | /com/ssokolow/QuickTile 113 | $ qdbus com.ssokolow.QuickTile /com/ssokolow/QuickTile 114 | method QString org.freedesktop.DBus.Introspectable.Introspect() 115 | method bool com.ssokolow.QuickTile.doCommand(QString command) 116 | $ qdbus com.ssokolow.QuickTile /com/ssokolow/QuickTile \ 117 | doCommand top-left 118 | true 119 | [terminal window is repositioned to the screen's top-left quarter] 120 | 121 | The more ubiquitous ``dbus-send`` command can also be used to accomplish the 122 | same thing, but it's much less convenient to work with and cannot double as 123 | a D-Bus browser: 124 | 125 | .. code-block:: shell-session 126 | 127 | $ dbus-send --type=method_call \ 128 | --dest=com.ssokolow.QuickTile \ 129 | /com/ssokolow/QuickTile \ 130 | com.ssokolow.QuickTile.doCommand \ 131 | string:top-left 132 | 133 | The :any:`bool` returned by ``doCommand`` indicates whether the given name 134 | was found in the list of registered tiling commands. 135 | 136 | Both of these commands can also be used as drop-in replacements for the 137 | command-line interface as long as ``quicktile --daemonize`` has been started 138 | beforehand. 139 | 140 | Regardless of how you invoke the D-Bus interface, it has two advantages over 141 | the command-line interface: 142 | 143 | * :command:`qdbus` and :command:`dbus-send` start more quickly than QuickTile, 144 | so this is likely to have lower latency even if being invoked from a shell 145 | script rather than doing a direct D-Bus call from a resident process to 146 | QuickTile. 147 | * Because it uses `-\\-daemonize `_ to spin up 148 | a persistent event loop shared by the D-Bus and X server client libraries, 149 | the D-Bus interface is demonstrably free from all race conditions currently 150 | known to affect the command-line interface. 151 | -------------------------------------------------------------------------------- /docs/wrench.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/docs/wrench.png -------------------------------------------------------------------------------- /functional_harness/__init__.py: -------------------------------------------------------------------------------- 1 | """Helper code for running QuickTile functional tests""" 2 | 3 | __author__ = "Stephan Sokolow (deitarion/SSokolow)" 4 | __license__ = "GNU GPL 2.0 or later" 5 | -------------------------------------------------------------------------------- /functional_harness/env_general.py: -------------------------------------------------------------------------------- 1 | """Assorted context managers for setting up the test environment""" 2 | 3 | __author__ = "Stephan Sokolow (deitarion/SSokolow)" 4 | __license__ = "MIT" 5 | 6 | import subprocess # nosec 7 | from contextlib import contextmanager 8 | 9 | # Silence PyLint being flat-out wrong about MyPy type annotations 10 | # pylint: disable=unsubscriptable-object 11 | 12 | # -- Type-Annotation Imports -- 13 | from typing import Any, Dict, Generator, Union # NOQA 14 | 15 | 16 | @contextmanager 17 | def background_proc(argv, env: Dict[str, Union[bytes, str]], verbose=False, 18 | *args: Any, **kwargs: Any 19 | ) -> Generator[None, None, None]: 20 | """Context manager for scoping the lifetime of a ``subprocess.Popen`` call 21 | 22 | :param argv: The command to be executed 23 | :param verbose: If :any:`False`, redirect the X server's ``stdout`` and 24 | ``stderr`` to :file:`/dev/null` 25 | :param args: Positional arguments to pass to :class:`subprocess.Popen` 26 | :param kwargs: Keyword arguments to pass to :class:`subprocess.Popen` 27 | """ 28 | if verbose: 29 | popen_obj = subprocess.Popen( # type: ignore # nosec 30 | argv, env=env, *args, **kwargs) 31 | else: 32 | popen_obj = subprocess.Popen(argv, # type: ignore # nosec 33 | stderr=subprocess.STDOUT, stdout=subprocess.DEVNULL, 34 | env=env, *args, **kwargs) 35 | try: 36 | yield popen_obj 37 | finally: 38 | popen_obj.terminate() 39 | -------------------------------------------------------------------------------- /functional_harness/x_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Wrapper for easily setting up and tearing down a test X server""" 3 | 4 | __author__ = "Stephan Sokolow (deitarion/SSokolow)" 5 | __license__ = "MIT" 6 | 7 | # Silence PyLint being flat-out wrong about MyPy type annotations 8 | # complaining about my grouped imports 9 | # pylint: disable=unsubscriptable-object,invalid-sequence-index 10 | # pylint: disable=wrong-import-order 11 | 12 | import logging, os, random, shutil, subprocess, tempfile # nosec 13 | from contextlib import contextmanager 14 | from distutils.spawn import find_executable 15 | 16 | # -- Type-Annotation Imports -- 17 | from typing import Dict, Generator, List, Tuple 18 | 19 | log = logging.getLogger(__name__) 20 | 21 | 22 | def _init_x_server(argv: List[str], verbose: bool = False 23 | ) -> Tuple[subprocess.Popen, bytes]: 24 | """Wrapper for starting an X server with the given command line 25 | 26 | :param argv: The command-line to execute 27 | :param verbose: If :any:`False`, redirect the X server's ``stdout`` and 28 | ``stderr`` to :file:`/dev/null` 29 | :returns: The process object for the X server. 30 | 31 | :raises subprocess.CalledProcessError: The X server exited with an 32 | unexpected error. 33 | """ 34 | 35 | # Launch the X server 36 | read_pipe, write_pipe = os.pipe() 37 | argv += ['+xinerama', '-displayfd', str(write_pipe)] 38 | 39 | env: Dict[str, str] = {} 40 | 41 | # pylint: disable=unexpected-keyword-arg,no-member 42 | if verbose: 43 | xproc = subprocess.Popen(argv, pass_fds=[write_pipe], env=env) # nosec 44 | else: 45 | xproc = subprocess.Popen(argv, pass_fds=[write_pipe], # nosec 46 | env=env, stderr=subprocess.STDOUT, stdout=subprocess.DEVNULL) 47 | 48 | display = os.read(read_pipe, 128).strip() 49 | return xproc, display 50 | 51 | 52 | @contextmanager 53 | def x_server(argv: List[str], screens: Dict[int, str] 54 | ) -> Generator[Dict[str, str], None, None]: 55 | """Context manager to launch and then clean up an X server. 56 | 57 | :param argv: The command to launch the test X server and 58 | any arguments not relating to defining the attached screens. 59 | :param screens: A :any:`dict ` mapping screen numbers to 60 | ``WxHxDEPTH`` strings. (eg. ``{0: '1024x768x32'}``) 61 | 62 | :raises subprocess.CalledProcessError: The X server or :command:`xauth` 63 | failed unexpectedly. 64 | :raises FileNotFoundError: Could not find either the :command:`xauth` 65 | command or ``argv[0]``. 66 | :raises PermissionError: Somehow, we lack write permission inside a 67 | directory created by :func:`tempfile.mkdtemp`. 68 | :raises ValueError: ``argv[0]`` was not an X server binary we know how to 69 | specify monitor rectangles for. 70 | (either :command:`Xvfb` or :command:`Xephyr`) 71 | :raises UnicodeDecodeError: The X server's ``-displayfd`` option wrote 72 | a value to the given FD which could not be decoded as UTF-8 when it 73 | should have been part of the 7-bit ASCII subset of UTF-8. 74 | 75 | .. todo:: Either don't accept an arbitrary ``argv`` string as input to 76 | :func:`x_server` or default to a behaviour likely to work with other X 77 | servers rather than erroring out. 78 | """ 79 | # Check for missing requirements 80 | for cmd in ['xauth', argv[0]]: 81 | if not find_executable(cmd): 82 | # pylint: disable=undefined-variable 83 | raise FileNotFoundError( # NOQA 84 | "Cannot find required command {!r}".format(cmd)) 85 | 86 | x_server = None 87 | tempdir = tempfile.mkdtemp() 88 | try: 89 | # Because random.getrandbits gets interpreted as a variable length, 90 | # *ensure* we've got the right number of hex digits 91 | magic_cookie = b'' 92 | while len(magic_cookie) < 32: 93 | magic_cookie += hex(random.getrandbits(128))[2:34].encode('ascii') 94 | magic_cookie = magic_cookie[:32] 95 | assert len(magic_cookie) == 32, len(magic_cookie) # nosec 96 | xauthfile = os.path.join(tempdir, 'Xauthority') 97 | env = {'XAUTHORITY': xauthfile} 98 | 99 | open(xauthfile, 'w').close() # create empty file 100 | 101 | # Convert `screens` into the format Xorg servers expect 102 | screen_argv = [] 103 | for screen_num, screen_geom in screens.items(): 104 | if 'Xvfb' in argv[0]: 105 | screen_argv.extend(['-screen', '%d' % screen_num, screen_geom]) 106 | elif 'Xephyr' in argv[0]: 107 | screen_argv.extend(['-screen', screen_geom]) 108 | else: 109 | raise ValueError("Unrecognized X server. Cannot infer format " 110 | "for specifying screen geometry.") 111 | 112 | # Initialize an X server on a free display number 113 | x_server, display_num = _init_x_server(argv + screen_argv) 114 | 115 | # Set up the environment and authorization 116 | env['DISPLAY'] = ':%s' % display_num.decode('utf8') 117 | subprocess.check_call( # nosec 118 | ['xauth', 'add', env['DISPLAY'], '.', magic_cookie], 119 | env=env) 120 | # FIXME: This xauth call once had a random failure. Retry. 121 | 122 | yield env 123 | 124 | finally: 125 | if x_server: 126 | x_server.terminate() 127 | shutil.rmtree(tempdir) 128 | -------------------------------------------------------------------------------- /icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 45 | 47 | 48 | 50 | image/svg+xml 51 | 53 | 54 | 55 | 56 | 61 | 69 | 77 | 85 | 89 | 95 | 101 | 107 | 113 | 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd "$(dirname "$(readlink -f "$0")")" 4 | 5 | if [ "$(id -u)" != 0 ]; then 6 | echo "* Building without elevated privileges" 7 | python3 setup.py build 8 | 9 | echo "* Acquiring permissions to perform system-wide install" 10 | exec sudo -H "$0" "$@" 11 | fi 12 | 13 | echo "* Attempting to remove old QuickTile installs" 14 | pip2 uninstall quicktile -y 15 | pip3 uninstall quicktile -y 16 | rm -f /usr/local/bin/quicktile{,.py} 17 | 18 | echo "* Running setup.py install" 19 | python3 setup.py install 20 | 21 | echo "* Copying quicktile.desktop to /etc/xdg/autostart/" 22 | sudo cp quicktile.desktop /etc/xdg/autostart/ 23 | -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # Specify a configuration file. 4 | #rcfile= 5 | 6 | # Python code to execute, usually for sys.path manipulation such as 7 | # pygtk.require(). 8 | #init-hook= 9 | 10 | # Add files or directories to the blacklist. They should be base names, not 11 | # paths. 12 | ignore=CVS 13 | 14 | # Pickle collected data for later comparisons. 15 | persistent=yes 16 | 17 | # List of plugins (as comma separated values of python modules names) to load, 18 | # usually to register additional checkers. 19 | load-plugins= 20 | 21 | 22 | [MESSAGES CONTROL] 23 | 24 | # Enable the message, report, category or checker with the given id(s). You can 25 | # either give multiple identifier separated by comma (,) or put this option 26 | # multiple time. 27 | #enable= 28 | 29 | # Disable the message, report, category or checker with the given id(s). You 30 | # can either give multiple identifier separated by comma (,) or put this option 31 | # multiple time (only on the command line, not in the configuration file where 32 | # it should appear only once). 33 | disable=I0011,W0142,W0621,bad-continuation,multiple-imports,wrong-import-position 34 | # I0011 Warning locally suppressed using disable-msg 35 | # I0012 Warning locally suppressed using disable-msg 36 | # W0142 Used * or * magic* Used when a function or method is called using *args or **kwargs to dispatch arguments. 37 | # W0621 Redefining name 'x' from outer scope 38 | 39 | 40 | [REPORTS] 41 | 42 | # Set the output format. Available formats are text, parseable, colorized, msvs 43 | # (visual studio) and html 44 | output-format=text 45 | 46 | # Put messages in a separate file for each module / package specified on the 47 | # command line instead of printing them on stdout. Reports (if any) will be 48 | # written in a file name "pylint_global.[txt|html]". 49 | files-output=no 50 | 51 | # Tells whether to display a full report or only the messages 52 | reports=no 53 | 54 | # Python expression which should return a note less than 10 (10 is the highest 55 | # note). You have access to the variables errors warning, statement which 56 | # respectively contain the number of errors / warnings messages and the total 57 | # number of statements analyzed. This is used by the global evaluation report 58 | # (RP0004). 59 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 60 | 61 | 62 | [SIMILARITIES] 63 | 64 | # Minimum lines number of a similarity. 65 | min-similarity-lines=4 66 | 67 | # Ignore comments when computing similarities. 68 | ignore-comments=yes 69 | 70 | # Ignore docstrings when computing similarities. 71 | ignore-docstrings=yes 72 | 73 | 74 | [MISCELLANEOUS] 75 | 76 | # List of note tags to take in consideration, separated by a comma. 77 | notes=FIXME,XXX,TODO 78 | 79 | 80 | [TYPECHECK] 81 | 82 | # Tells whether missing members accessed in mixin class should be ignored. A 83 | # mixin class is detected if its name ends with "mixin" (case insensitive). 84 | ignore-mixin-members=yes 85 | 86 | # List of classes names for which member attributes should not be checked 87 | # (useful for classes with attributes dynamically set). 88 | ignored-classes=SQLObject 89 | 90 | # List of members which are set dynamically and missed by pylint inference 91 | # system, and so shouldn't trigger E0201 when accessed. Python regular 92 | # expressions are accepted. 93 | generated-members=REQUEST,acl_users,aq_parent 94 | 95 | 96 | [VARIABLES] 97 | 98 | # Tells whether we should check for unused import in __init__ files. 99 | init-import=yes 100 | 101 | # A regular expression matching the beginning of the name of dummy variables 102 | # (i.e. not used). 103 | dummy-variables-rgx=_|dummy 104 | 105 | # List of additional names supposed to be defined in builtins. Remember that 106 | # you should avoid to define new builtins when possible. 107 | additional-builtins= 108 | 109 | 110 | [BASIC] 111 | 112 | # List of builtins function names that should not be used, separated by a comma 113 | bad-functions=map,filter,apply,input 114 | 115 | # Regular expression which should only match correct module names 116 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 117 | 118 | # Regular expression which should only match correct module level names 119 | const-rgx=(([a-z_][a-zA-Z1-9_]*)|([A-Z_][A-Z1-9_]*)|(__.*__))$ 120 | 121 | # Regular expression which should only match correct class names 122 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 123 | 124 | # Regular expression which should only match correct function names 125 | function-rgx=[a-z_][a-zA-Z0-9_]{2,30}$ 126 | 127 | # Regular expression which should only match correct method names 128 | method-rgx=([a-z_][a-zA-Z0-9]{2,30}|__[a-z0-9_]+__|[a-z_][a-z0-9_]{2,30})$ 129 | 130 | # Regular expression which should only match correct instance attribute names 131 | attr-rgx=[a-z_]([a-zA-Z0-9]{2,30}|[a-z0-9_]{2,30})$ 132 | 133 | # Regular expression which should only match correct argument names 134 | argument-rgx=[a-z_][a-zA-Z0-9_]{2,30}$ 135 | 136 | # Regular expression which should only match correct variable names 137 | variable-rgx=[a-z_][a-zA-Z0-9_]{2,30}$ 138 | 139 | # Regular expression which should only match correct list comprehension / 140 | # generator expression variable names 141 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 142 | 143 | # Good variable names which should always be accepted, separated by a comma 144 | good-names=i,j,k,ex,Run,_,x,y,w,h,r 145 | # i, j, k (Iterators) 146 | # ex, Run (TODO: Do I want to keep these?) 147 | # _ (Throaway result) 148 | # x, y, w, h, r (Drawing Coordinates) 149 | 150 | # Bad variable names which should always be refused, separated by a comma 151 | bad-names=foo,bar,baz,toto,tutu,tata,spam,eggs,ham,qux,quux,glorp 152 | 153 | # Regular expression which should only match functions or classes name which do 154 | # not require a docstring 155 | no-docstring-rgx=__.*__ 156 | 157 | 158 | [FORMAT] 159 | 160 | # Maximum number of characters on a single line. 161 | max-line-length=79 162 | 163 | # Maximum number of lines in a module 164 | max-module-lines=1000 165 | 166 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 167 | # tab). 168 | indent-string=' ' 169 | 170 | 171 | [DESIGN] 172 | 173 | # Maximum number of arguments for function / method 174 | max-args=5 175 | 176 | # Argument names that match this expression will be ignored. Default to name 177 | # with leading underscore 178 | ignored-argument-names=_.* 179 | 180 | # Maximum number of locals for function / method body 181 | max-locals=15 182 | 183 | # Maximum number of return / yield for function / method body 184 | max-returns=6 185 | 186 | # Maximum number of branch for function / method body 187 | max-branchs=12 188 | 189 | # Maximum number of statements in function / method body 190 | max-statements=50 191 | 192 | # Maximum number of parents for a class (see R0901). 193 | max-parents=7 194 | 195 | # Maximum number of attributes for a class (see R0902). 196 | max-attributes=7 197 | 198 | # Minimum number of public methods for a class (see R0903). 199 | min-public-methods=2 200 | 201 | # Maximum number of public methods for a class (see R0904). 202 | max-public-methods=20 203 | 204 | 205 | [CLASSES] 206 | 207 | # List of method names used to declare (i.e. assign) instance attributes. 208 | defining-attr-methods=__init__,__new__,setUp 209 | 210 | # List of valid names for the first argument in a class method. 211 | valid-classmethod-first-arg=cls 212 | 213 | 214 | [IMPORTS] 215 | 216 | # Deprecated modules which should not be used, separated by a comma 217 | deprecated-modules=regsub,string,TERMIOS,Bastion,rexec 218 | 219 | # Create a graph of every (i.e. internal and external) dependencies in the 220 | # given file (report RP0402 must not be disabled) 221 | import-graph= 222 | 223 | # Create a graph of external dependencies in the given file (report RP0402 must 224 | # not be disabled) 225 | ext-import-graph= 226 | 227 | # Create a graph of internal dependencies in the given file (report RP0402 must 228 | # not be disabled) 229 | int-import-graph= 230 | 231 | extension-pkg-whitelist=gtk,pango,PyQt4 232 | 233 | [EXCEPTIONS] 234 | 235 | # Exceptions that will emit a warning when being caught. Defaults to 236 | # "Exception" 237 | overgeneral-exceptions=Exception 238 | -------------------------------------------------------------------------------- /quicktile.desktop: -------------------------------------------------------------------------------- 1 | 2 | [Desktop Entry] 3 | Encoding=UTF-8 4 | Version=1.0 5 | Name=QuickTile 6 | GenericName=Window-Tiling Helper 7 | Type=Application 8 | Exec=quicktile --daemonize 9 | Categories=Utility; 10 | -------------------------------------------------------------------------------- /quicktile.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd "$(dirname "$(readlink -f "$0")")" 3 | exec python3 -m quicktile "$@" 4 | -------------------------------------------------------------------------------- /quicktile/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """QuickTile 4 | Keyboard-driven Window Tiling for your existing X11 window manager 5 | 6 | :newfield appname: Application Name 7 | """ 8 | 9 | __author__ = "Stephan Sokolow (deitarion/SSokolow)" 10 | __appname__ = "QuickTile" 11 | __license__ = "GNU GPL 2.0 or later" 12 | 13 | # vim: set sw=4 sts=4 expandtab : 14 | -------------------------------------------------------------------------------- /quicktile/config.py: -------------------------------------------------------------------------------- 1 | """Configuration parsing code""" 2 | 3 | import logging, os 4 | from configparser import ConfigParser 5 | 6 | from typing import Dict, Union 7 | 8 | #: Location for config files (determined at runtime). 9 | XDG_CONFIG_DIR = os.environ.get('XDG_CONFIG_HOME', 10 | os.path.expanduser('~/.config')) 11 | 12 | #: MyPy type alias for fields loaded from config files 13 | CfgDict = Dict[str, Union[str, int, float, bool, None]] # pylint:disable=C0103 14 | 15 | #: Default content for the configuration file 16 | #: 17 | #: .. todo:: Figure out a way to show :data:`DEFAULTS` documentation but with 18 | #: the structure pretty-printed. 19 | DEFAULTS: Dict[str, CfgDict] = { 20 | 'general': { 21 | # Use Ctrl+Alt as the default base for key combinations 22 | 'ModMask': '', 23 | 'MovementsWrap': True, 24 | 'ColumnCount': 3, 25 | 'MarginX_Percent': 0, 26 | 'MarginY_Percent': 0, 27 | }, 28 | 'keys': { 29 | "KP_Enter": "monitor-switch", 30 | "KP_0": "maximize", 31 | "KP_1": "bottom-left", 32 | "KP_2": "bottom", 33 | "KP_3": "bottom-right", 34 | "KP_4": "left", 35 | "KP_5": "center", 36 | "KP_6": "right", 37 | "KP_7": "top-left", 38 | "KP_8": "top", 39 | "KP_9": "top-right", 40 | "KP_1": "move-to-bottom-left", 41 | "KP_2": "move-to-bottom", 42 | "KP_3": "move-to-bottom-right", 43 | "KP_4": "move-to-left", 44 | "KP_5": "move-to-center", 45 | "KP_6": "move-to-right", 46 | "KP_7": "move-to-top-left", 47 | "KP_8": "move-to-top", 48 | "KP_9": "move-to-top-right", 49 | "V": "vertical-maximize", 50 | "H": "horizontal-maximize", 51 | "C": "move-to-center", 52 | } 53 | } 54 | 55 | #: Used for resolving certain keysyms 56 | #: 57 | #: .. todo:: Figure out how to replace :data:`KEYLOOKUP` with a fallback that 58 | #: uses something in `Gtk `_ or 59 | #: ``python-xlib`` to look up the keysym from the character it types. 60 | KEYLOOKUP = { 61 | ',': 'comma', 62 | '.': 'period', 63 | '+': 'plus', 64 | '-': 'minus', 65 | } 66 | 67 | 68 | def load_config(path) -> ConfigParser: 69 | """Load the config file from the given path, applying fixes as needed. 70 | If it does not exist, create it from the configuration defaults. 71 | 72 | :param path: The path to load or initialize. 73 | 74 | :raises TypeError: Raised if the keys or values in the :ref:`[keys]` 75 | section of the configuration file or what they resolve to via 76 | :any:`KEYLOOKUP` are not :any:`str` instances. 77 | 78 | .. todo:: Refactor config parsing. It's an ugly blob. 79 | """ 80 | first_run = not os.path.exists(path) 81 | 82 | config = ConfigParser(interpolation=None) 83 | 84 | # Make keys case-sensitive because keysyms must be 85 | # 86 | # (``type: ignore`` to squash a false positive for something the Python 3.x 87 | # documentation specifically *recommends* over using RawConfigParser) 88 | config.optionxform = str # type: ignore 89 | 90 | config.read(path) 91 | dirty = False 92 | 93 | if not config.has_section('general'): 94 | config.add_section('general') 95 | # Change this if you make backwards-incompatible changes to the 96 | # section and key naming in the config file. 97 | config.set('general', 'cfg_schema', '1') 98 | dirty = True 99 | 100 | # Transparently update the config to add missing keys 101 | for key, val in DEFAULTS['general'].items(): 102 | if not config.has_option('general', key): 103 | config.set('general', key, str(val)) 104 | dirty = True 105 | 106 | mk_raw = config.get('general', 'ModMask') 107 | modkeys = mk_raw.strip() # pylint: disable=E1101 108 | if ' ' in modkeys and '<' not in modkeys: 109 | modkeys = '<%s>' % '><'.join(modkeys.split()) 110 | logging.info("Updating modkeys format:\n %r --> %r", mk_raw, modkeys) 111 | config.set('general', 'ModMask', modkeys) 112 | dirty = True 113 | 114 | # Either load the keybindings or use and save the defaults 115 | if config.has_section('keys'): 116 | keymap: CfgDict = dict(config.items('keys')) 117 | else: 118 | keymap = DEFAULTS['keys'] 119 | config.add_section('keys') 120 | for key, cmd in keymap.items(): 121 | if not isinstance(key, str): # pragma: nobranch 122 | raise TypeError( # pragma: nocover 123 | "Hotkey name must be a str: {!r}".format(key)) 124 | if not isinstance(cmd, str): # pragma: nobranch 125 | raise TypeError( # pragma: nocover 126 | "Command name must be a str: {!r}".format(cmd)) 127 | config.set('keys', key, cmd) 128 | dirty = True 129 | 130 | # Migrate from the deprecated syntax for punctuation keysyms 131 | for key in keymap: 132 | # Look up unrecognized shortkeys in a hardcoded dict and 133 | # replace with valid names like ',' -> 'comma' 134 | if key in KEYLOOKUP: 135 | cmd = keymap[key] 136 | if not isinstance(cmd, str): # pragma: nobranch 137 | raise TypeError( # pragma: nocover 138 | "Command name must be a str: {!r}".format(cmd)) 139 | 140 | logging.warning("Updating config file from deprecated keybind " 141 | "syntax:\n\t%r --> %r", key, KEYLOOKUP[key]) 142 | config.remove_option('keys', key) 143 | config.set('keys', KEYLOOKUP[key], cmd) 144 | dirty = True 145 | 146 | # Automatically update the old 'middle' command to 'center' 147 | for key in keymap: 148 | if keymap[key] == 'middle': 149 | keymap[key] = cmd = 'center' 150 | logging.warning("Updating old command in config file:" 151 | "\n\tmiddle --> center") 152 | config.set('keys', key, cmd) 153 | dirty = True 154 | 155 | if dirty: 156 | with open(path, 'w') as cfg_file: 157 | config.write(cfg_file) 158 | if first_run: 159 | logging.info("Wrote default config file to %s", path) 160 | 161 | return config 162 | -------------------------------------------------------------------------------- /quicktile/dbus_api.py: -------------------------------------------------------------------------------- 1 | """D-Bus API for controlling QuickTile""" 2 | 3 | __author__ = "Stephan Sokolow (deitarion/SSokolow)" 4 | __license__ = "GNU GPL 2.0 or later" 5 | 6 | # Silence PyLint about my grouped imports 7 | # pylint: disable=wrong-import-order 8 | 9 | import logging 10 | 11 | from dbus.service import BusName, Object, method 12 | from dbus import SessionBus 13 | from dbus.exceptions import DBusException 14 | from dbus.mainloop.glib import DBusGMainLoop 15 | 16 | # -- Type-Annotation Imports -- 17 | from typing import Optional, Tuple 18 | from .commands import CommandRegistry 19 | from .wm import WindowManager 20 | # -- 21 | 22 | 23 | class QuickTile(Object): 24 | """D-Bus endpoint definition 25 | 26 | :param bus: The connection on which to export this object. 27 | See the :class:`dbus.service.Object` documentation for details. 28 | """ 29 | def __init__(self, 30 | bus: SessionBus, 31 | commands: CommandRegistry, 32 | winman: WindowManager) -> None: 33 | Object.__init__(self, bus, '/com/ssokolow/QuickTile') 34 | self.commands = commands 35 | self.winman = winman 36 | 37 | @method(dbus_interface='com.ssokolow.QuickTile', 38 | in_signature='s', out_signature='b') 39 | def doCommand(self, command: str) -> bool: 40 | """Execute a QuickTile tiling command 41 | 42 | :param command: The name of the command to attempt to run. 43 | :returns: Whether ``command`` was found in the registry. 44 | 45 | .. todo:: Expose a proper, introspectable D-Bus API. 46 | .. todo:: When I'm willing to break the external API, retire the 47 | :meth:`doCommand` name. 48 | """ 49 | return self.commands.call(command, self.winman) 50 | 51 | 52 | def init(commands: CommandRegistry, 53 | winman: WindowManager, 54 | ) -> Optional[Tuple[BusName, QuickTile]]: 55 | """Initialize the DBus backend 56 | 57 | This handles hooking D-Bus into the Glib main loop, connecting to the 58 | session bus, and creating a :class:`QuickTile` instance.""" 59 | try: 60 | DBusGMainLoop(set_as_default=True) 61 | sess_bus = SessionBus() 62 | except DBusException: 63 | logging.warning("Could not connect to the D-Bus Session Bus.") 64 | return None 65 | 66 | dbus_name = BusName("com.ssokolow.QuickTile", sess_bus) 67 | dbus_obj = QuickTile(sess_bus, commands, winman) 68 | 69 | return dbus_name, dbus_obj 70 | -------------------------------------------------------------------------------- /quicktile/version.py: -------------------------------------------------------------------------------- 1 | """Common definition of the version for use in various places""" 2 | 3 | __author__ = "Stephan Sokolow (deitarion/SSokolow)" 4 | __license__ = "GNU GPL 2.0 or later" 5 | __version__ = "0.4.1" 6 | 7 | # vim: set sw=4 sts=4 expandtab : 8 | -------------------------------------------------------------------------------- /recompile_local_debian_package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cat < 23 | 24 | ======================================================================== 25 | 26 | Let's go! 27 | 28 | 29 | EOF 30 | 31 | set -euo pipefail 32 | 33 | cd "$(dirname "$(readlink -f "$0")" )" 34 | 35 | dpkg-checkbuilddeps 36 | echo -e "* dpkg-checkbuilddeps\tPASSED" 37 | 38 | PKGDIR="$PWD" 39 | 40 | TMPDIR=$( mktemp -d ) && echo "* Will work in temp dir $TMPDIR" 41 | 42 | CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) && echo "* Current git branch $CURRENT_BRANCH" 43 | 44 | NAMEFORTAR="$( head -n 1 debian/changelog | sed -n 's/^\([^ ]*\)* (\([^-]*\)-[0-9]*).*$/\1_\2/p' )" 45 | DIRNAMEFORDEB=${NAMEFORTAR//_/-} 46 | 47 | if [[ -z "$NAMEFORTAR" ]] 48 | then 49 | echo >&2 "Cannot figure out tar archive name from first line of debian/changelog. Aborting" 50 | head -n 1 debian/changelog 51 | exit 1 52 | fi 53 | 54 | if output=$(git status --porcelain) && [ -z "$output" ]; then 55 | echo "Working directory clean" 56 | else 57 | echo >&2 "WARNING: uncommitted changes. Consider aborting." 58 | git status 59 | echo >&2 "WARNING: uncommitted changes. Consider aborting." 60 | echo >&2 "Waiting for 10 second." 61 | sleep 10 62 | fi 63 | 64 | GITREV=$( git describe ) 65 | 66 | cd "$TMPDIR" 67 | git clone "$PKGDIR" "${DIRNAMEFORDEB}" 68 | 69 | tar zcvf ${NAMEFORTAR}.orig.tar.gz "${DIRNAMEFORDEB}" 70 | cd "${DIRNAMEFORDEB}" 71 | 72 | dpkg-checkbuilddeps 73 | debuild -us -uc 74 | 75 | . /etc/os-release ; DISTRO_ID="${ID}-${VERSION_ID}" 76 | 77 | OUTDIR="$PKGDIR/../compiled_packages/${DISTRO_ID}/${NAMEFORTAR}" #_$( date +%Yy%mm%dd_%Hh%Mm%Ss )" 78 | 79 | mkdir -p "$OUTDIR" 80 | 81 | echo "generated from git commit $GITREV" >"${OUTDIR}/${NAMEFORTAR}.gitversion" 82 | 83 | cd .. 84 | 85 | cp -v "${NAMEFORTAR}"?* "$OUTDIR" 86 | 87 | echo 88 | echo ================================================================ 89 | echo "Artifacts available in $OUTDIR:" 90 | echo "OUTDIR=$OUTDIR" 91 | echo ================================================================ 92 | 93 | cd "$OUTDIR" 94 | 95 | ls -al 96 | -------------------------------------------------------------------------------- /run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd "$(dirname "$(readlink -f "$0")")" 4 | echo "-- MyPy --" 5 | MYPYPATH="quicktile" mypy --config-file=setup.cfg quicktile ./*.py functional_harness 6 | echo " -- Flake8 (static analysis) --" 7 | python3 -m flake8 --config=setup.cfg quicktile/ ./*.py functional_harness 8 | echo "-- Nose (unit tests) --" 9 | nosetests3 "$@" 10 | echo "-- Sphinx (documentation syntax) --" 11 | cd docs 12 | make coverage | grep -v 'Testing of coverage in the sources finished' 13 | cat _build/coverage/python.txt 14 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | show-pep8 = true 3 | show-source = true 4 | ignore = N802,E126,E128,E221,E302,E401,E402,W504 5 | 6 | [mypy] 7 | ignore_missing_imports = true 8 | strict_optional = true 9 | warn_unused_ignores = true 10 | warn_return_any = true 11 | warn_unreachable = true 12 | 13 | [nosetests] 14 | with-doctest = true 15 | with-coverage = true 16 | cover-tests = true 17 | cover-branches = true 18 | cover-erase = true 19 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Standard setuptools build/install script 3 | 4 | @todo: 5 | - Identify minimum dependency versions properly. 6 | """ 7 | 8 | from __future__ import print_function 9 | 10 | __author__ = "Stephan Sokolow (deitarion/SSokolow)" 11 | __license__ = "GNU GPL 2.0 or later" 12 | __docformat__ = "restructuredtext en" 13 | 14 | import io, os, re 15 | from setuptools import setup 16 | 17 | try: 18 | import gi 19 | except ImportError: 20 | print("WARNING: Could not import PyGI. You will need to it via your " 21 | "package manager (eg. `sudo apt-get install python3-gi`) before you " 22 | "will be able to run QuickTile.") 23 | else: 24 | for name in ['Gdk', 'GdkX11', 'Wnck']: 25 | try: 26 | gi.require_version(name, '3.0') 27 | except ValueError: 28 | print("WARNING: Could not load the PyGI bindings for %s. You will " 29 | "need to install them before you will be able to run " 30 | "QuickTile." % name) 31 | del name 32 | 33 | # TODO: Switch to PyGI-based D-Bus support 34 | 35 | # Get the version from the program rather than duplicating it here 36 | # Source: https://packaging.python.org/en/latest/single_source_version.html 37 | 38 | 39 | def read(*names, **kwargs): 40 | """Convenience wrapper for ``read()``-ing a file in one call""" 41 | with io.open(os.path.join(os.path.dirname(__file__), *names), 42 | encoding=kwargs.get("encoding", "utf8")) as fobj: 43 | return fobj.read() 44 | 45 | 46 | def find_version(*file_paths): 47 | """Extract the value of ``__version__`` from the given file""" 48 | version_file = read(*file_paths) 49 | version_match = re.search(r"^__version__\s*=\s*['\"]([^'\"]*)['\"]", 50 | version_file, re.M) 51 | if version_match: 52 | return version_match.group(1) 53 | raise RuntimeError("Unable to find version string.") 54 | 55 | 56 | if __name__ == '__main__': 57 | setup( 58 | name='QuickTile', 59 | version=find_version("quicktile", "version.py"), 60 | author='Stephan Sokolow (deitarion/SSokolow)', 61 | author_email='http://ssokolow.com/ContactMe', 62 | description='Add keyboard-driven window-tiling to any X11 window ' 63 | 'manager (inspired by WinSplit Revolution)', 64 | long_description=read("README.rst"), 65 | url="http://ssokolow.com/quicktile/", 66 | 67 | classifiers=[ 68 | 'Development Status :: 4 - Beta', 69 | 'Environment :: X11 Applications :: GTK', 70 | 'Intended Audience :: End Users/Desktop', 71 | 'License :: OSI Approved :: GNU General Public License v2 or later' 72 | ' (GPLv2+)', 73 | 'Natural Language :: English', 74 | 'Operating System :: POSIX', 75 | 'Programming Language :: Python :: 3 :: Only', 76 | 'Topic :: Desktop Environment :: Window Managers', 77 | 'Topic :: Utilities', 78 | ], 79 | keywords=('x11 desktop window tiling wm utility addon extension tile ' 80 | 'layout positioning helper keyboard hotkey hotkeys shortcut ' 81 | 'shortcuts tool'), 82 | license="GPL-2.0+", 83 | 84 | install_requires=['python-xlib'], 85 | 86 | packages=['quicktile'], 87 | entry_points={ 88 | 'console_scripts': ['quicktile=quicktile.__main__:main'] 89 | }, 90 | data_files=[('share/applications', ['quicktile.desktop'])] 91 | ) 92 | 93 | # vim: set sw=4 sts=4 expandtab : 94 | -------------------------------------------------------------------------------- /test_functional.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """Beginnings of a functional test harness for QuickTile 4 | 5 | .. todo:: Don't forget to test unusual configurations such as: 6 | 7 | 1. ``monitor-*`` commands with only one monitor 8 | 2. ``workspace-*`` commands with only one workspace 9 | 3. Having screens 1, 2, and 4 but not 0 or 3 (eg. hotplug aftermath) 10 | 4. Having no windows on the desktop 11 | 5. Having no window manager (with and without windows) 12 | 6. Various Xinerama layouts 13 | 7. Test with Xinerama disabled 14 | """ 15 | 16 | from __future__ import (absolute_import, division, print_function, 17 | with_statement, unicode_literals) 18 | 19 | __author__ = "Stephan Sokolow (deitarion/SSokolow)" 20 | __license__ = "MIT" 21 | 22 | #: The sequence of commands to call QuickTile with 23 | TEST_SCRIPT = """ 24 | monitor-next-all 25 | monitor-prev-all 26 | monitor-switch-all 27 | monitor-prev-all 28 | 29 | monitor-next 30 | monitor-prev 31 | monitor-switch 32 | monitor-prev 33 | 34 | bottom 35 | bottom-left 36 | bottom-right 37 | left 38 | center 39 | right 40 | top 41 | top-left 42 | top-right 43 | 44 | move-to-bottom 45 | move-to-bottom-left 46 | move-to-bottom-right 47 | move-to-center 48 | move-to-left 49 | move-to-right 50 | move-to-top 51 | move-to-top-left 52 | move-to-top-right 53 | 54 | bordered 55 | bordered 56 | 57 | always-above 58 | always-above 59 | always-below 60 | always-below 61 | horizontal-maximize 62 | horizontal-maximize 63 | vertical-maximize 64 | vertical-maximize 65 | shade 66 | shade 67 | fullscreen 68 | fullscreen 69 | all-desktops 70 | all-desktops 71 | 72 | trigger-move 73 | trigger-resize 74 | 75 | workspace-send-down 76 | workspace-go-down 77 | 78 | workspace-send-up 79 | workspace-go-up 80 | 81 | workspace-send-left 82 | workspace-go-left 83 | 84 | workspace-send-right 85 | workspace-go-right 86 | 87 | workspace-send-next 88 | workspace-go-next 89 | 90 | workspace-send-prev 91 | workspace-go-prev 92 | 93 | show-desktop 94 | show-desktop 95 | 96 | maximize 97 | maximize 98 | 99 | minimize 100 | """ 101 | 102 | import logging, shlex, subprocess # nosec 103 | 104 | from functional_harness.env_general import background_proc 105 | from functional_harness.x_server import x_server 106 | 107 | log = logging.getLogger(__name__) 108 | 109 | 110 | def run_tests(env): 111 | """Run the old bank of 'commands don't crash' tests""" 112 | lines = [x.split('#')[0].strip() for x in TEST_SCRIPT.split('\n')] 113 | lines = [x for x in lines if x] 114 | for pos, command in enumerate(lines): 115 | log.info("Testing command %d of %d: %s", pos + 1, len(lines), command) 116 | subprocess.check_call(['./quicktile.sh', '--no-excepthook', 117 | command], env=env) # nosec 118 | 119 | 120 | def main(): 121 | """The main entry point, compatible with setuptools entry points.""" 122 | from argparse import ArgumentParser, RawDescriptionHelpFormatter 123 | parser = ArgumentParser(formatter_class=RawDescriptionHelpFormatter, 124 | description='Functional test runner for QuickTile', 125 | epilog="NOTE: Running tests under the Xephyr X server will change " 126 | "their behaviour if they depend on Xvfb's ability to present " 127 | "a desktop with non-sequentially numbered screens.") 128 | parser.add_argument('-v', '--verbose', action="count", 129 | default=2, help="Increase the verbosity. Use twice for extra effect.") 130 | parser.add_argument('-q', '--quiet', action="count", 131 | default=0, help="Decrease the verbosity. Use twice for extra effect.") 132 | parser.add_argument('-X', '--x-server', default="Xvfb", metavar="CMD", 133 | help="The X server to launch for testing (Try 'Xephyr' to debug tests " 134 | "using a live view. The default is '%(default)s'.)") 135 | # Reminder: %(default)s can be used in help strings. 136 | 137 | args = parser.parse_args() 138 | 139 | # Set up clean logging to stderr 140 | log_levels = [logging.CRITICAL, logging.ERROR, logging.WARNING, 141 | logging.INFO, logging.DEBUG] 142 | args.verbose = min(args.verbose - args.quiet, len(log_levels) - 1) 143 | args.verbose = max(args.verbose, 0) 144 | logging.basicConfig(level=log_levels[args.verbose], 145 | format='%(levelname)s: %(message)s') 146 | 147 | log.warning("This does not currently check the results of the tiling " 148 | "operations it requests. As such, it serves only as a way to check " 149 | "for uncaught exceptions being raised in code that isn't yet " 150 | "unit tested.") 151 | log.warning("TODO: Inject a test window into the nested X session so " 152 | "non-windowless commands don't bail out in the common code.") 153 | log.info("Starting test instance of %s", args.x_server) 154 | with x_server(shlex.split(args.x_server), 155 | {0: '1024x768x24', 1: '800x600x24'}) as env: 156 | # TODO: Use a custom configuration file for Openbox 157 | # TODO: Detect once the window manager has started and wait for that 158 | # before running the tests. 159 | # TODO: Proper test windows. 160 | log.info("Starting test copy of Openbox...") 161 | with background_proc(['openbox', '--startup', 'zenity --info'], env): 162 | # TODO: Rework so the process holding the session open can just 163 | # *report* when it's ready. 164 | log.info("Sleeping for 5 seconds...") 165 | import time 166 | time.sleep(5) 167 | run_tests(env) 168 | 169 | 170 | if __name__ == '__main__': 171 | main() 172 | 173 | # vim: set sw=4 sts=4 expandtab : 174 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/quicktile/30cf42d3fe2efc1ca8bf5b354e82413ad0fdf046/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_quicktile.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Unit Test Suite for QuickTile using Nose test discovery""" 3 | 4 | __author__ = "Stephan Sokolow (deitarion/SSokolow)" 5 | __license__ = "GNU GPL 2.0 or later" 6 | 7 | # TODO: I need a functional test to make sure issue #25 doesn't regress 8 | 9 | import configparser, logging, os, shutil, tempfile, unittest 10 | 11 | # Ensure code coverage counts modules not yet imported by tests. 12 | from quicktile import config, __main__ # NOQA pylint: disable=unused-import 13 | 14 | log = logging.getLogger(__name__) 15 | 16 | # class TestCommandRegistry(unittest.TestCase): 17 | # """Tests for the `CommandRegistry` class""" 18 | # def setUp(self): 19 | # self.registry = commands.CommandRegistry() 20 | # 21 | # # TODO: Implement tests for CommandRegistry 22 | 23 | # TODO: Implement tests for cycle_dimensions 24 | # TODO: Implement tests for cycle_monitors 25 | # TODO: Implement tests for move_to_position 26 | # TODO: Implement tests for toggle_decorated 27 | # TODO: Implement tests for toggle_desktop 28 | # TODO: Implement tests for toggle_state 29 | # TODO: Implement tests for trigger_keyboard_action 30 | # TODO: Implement tests for workspace_go 31 | # TODO: Implement tests for workspace_send_window 32 | 33 | # TODO: Implement tests for KeyBinder 34 | 35 | # TODO: Implement tests for QuickTileApp 36 | 37 | 38 | class ConfigV1Test(unittest.TestCase): 39 | """Tests for parsing ConfigParser-era quicktile.cfg files""" 40 | 41 | def setUp(self): 42 | """Set up scratch space for config files at the start of each unit""" 43 | self.testdir = tempfile.mkdtemp(prefix='quicktile-config-test-') 44 | self.testpath = os.path.join(self.testdir, 'test.config') 45 | 46 | def tearDown(self): 47 | """Delete the scratch space when each test is finished""" 48 | shutil.rmtree(self.testdir) 49 | 50 | def test_creates_on_first_run(self): 51 | """Config file is created if it doesn't exist""" 52 | self.assertFalse(os.path.exists(self.testpath)) 53 | parsed = config.load_config(self.testpath) 54 | self.assertTrue(os.path.exists(self.testpath)) 55 | 56 | # Verify all defaults were put in correctly 57 | for section, lines in config.DEFAULTS.items(): 58 | for key, value in lines.items(): 59 | self.assertEqual(str(value), parsed.get(section, key)) 60 | 61 | # Verify nothing but defaults and cfg_schema were put in 62 | for section in parsed.sections(): 63 | for option in parsed.options(section): 64 | if option == 'cfg_schema': 65 | self.assertEqual(parsed.get(section, option), '1') 66 | else: 67 | self.assertEqual( 68 | parsed.get(section, option), 69 | str(config.DEFAULTS[section][option])) 70 | 71 | def test_keys_are_case_sensitive(self): 72 | """Config keys are case-sensitive""" 73 | parsed = config.load_config(self.testpath) 74 | 75 | self.assertEqual('True', parsed.get('general', 'MovementsWrap')) 76 | self.assertRaises(configparser.NoSectionError, 77 | parsed.get, 'General', 'movementswrap') 78 | self.assertRaises(configparser.NoOptionError, 79 | parsed.get, 'general', 'movementswrap') 80 | 81 | # TODO: Check schema version 82 | 83 | def test_updating_modkeys_format(self): 84 | """ModKeys format is updated correctly""" 85 | with open(self.testpath, 'w') as fobj: 86 | fobj.write("[general]\ncfg_schema = 1\nModMask = Ctrl Alt") 87 | 88 | parsed = config.load_config(self.testpath) 89 | self.assertEqual("", parsed.get('general', 'ModMask')) 90 | 91 | # Test twice to guard against hard-coded overwrites 92 | with open(self.testpath, 'w') as fobj: 93 | fobj.write("[general]\ncfg_schema = 1\nModMask = Ctrl Shift") 94 | 95 | parsed = config.load_config(self.testpath) 96 | self.assertEqual("", parsed.get('general', 'ModMask')) 97 | 98 | def test_loading_keybindgs(self): 99 | """Keybinds not pulled from DEFAULTS load correctly""" 100 | # Assert default different than test value 101 | parsed = config.load_config(self.testpath) 102 | self.assertEqual("monitor-switch", parsed.get('keys', 'KP_Enter')) 103 | 104 | with open(self.testpath, 'w') as fobj: 105 | fobj.write("[keys]\nKP_Enter = maximize") 106 | 107 | # Assert test value 108 | parsed = config.load_config(self.testpath) 109 | self.assertEqual("maximize", parsed.get('keys', 'KP_Enter')) 110 | 111 | def test_remapping_invalid_keynames(self): 112 | """Punctuation keybinds get remapped to keysyms""" 113 | # Assert defaults different than test value 114 | parsed = config.load_config(self.testpath) 115 | for name in (',', '.', '+', '-', 'comma', 'period', 'plus', 'minus'): 116 | self.assertRaises(configparser.NoOptionError, 117 | parsed.get, 'keys', name) 118 | 119 | with open(self.testpath, 'w') as fobj: 120 | fobj.write("[keys]\n, = left\n. = right\n+ = top\n- = bottom") 121 | 122 | # Assert test values 123 | parsed = config.load_config(self.testpath) 124 | for name in (',', '.', '+', '-'): 125 | self.assertRaises(configparser.NoOptionError, 126 | parsed.get, 'keys', name) 127 | for name, mapping in ( 128 | ('comma', 'left'), 129 | ('period', 'right'), 130 | ('plus', 'top'), 131 | ('minus', 'bottom')): 132 | self.assertEqual(parsed.get('keys', name), mapping) 133 | 134 | # Verify the changes got committed to disk 135 | reparsed = config.load_config(self.testpath) 136 | for name, mapping in ( 137 | ('comma', 'left'), 138 | ('period', 'right'), 139 | ('plus', 'top'), 140 | ('minus', 'bottom')): 141 | self.assertEqual(reparsed.get('keys', name), mapping) 142 | 143 | def test_remapping_middle(self): 144 | """'middle' action automatically gets remapped to 'center'""" 145 | # Assert defaults different than test value 146 | parsed = config.load_config(self.testpath) 147 | self.assertEqual("left", parsed.get('keys', 'KP_4')) 148 | 149 | with open(self.testpath, 'w') as fobj: 150 | fobj.write("[keys]\nKP_4 = middle") 151 | 152 | # Assert test values 153 | parsed = config.load_config(self.testpath) 154 | self.assertEqual("center", parsed.get('keys', 'KP_4')) 155 | 156 | # Verify the changes got committed to disk 157 | with open(self.testpath, 'r') as fobj: 158 | self.assertTrue('\nKP_4 = center\n' in fobj.read()) 159 | 160 | # vim: set sw=4 sts=4 expandtab : 161 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py35 3 | 4 | [testenv] 5 | sitepackages = true 6 | deps= 7 | mypy 8 | nose 9 | coverage 10 | flake8 11 | commands= 12 | python3 -m mypy quicktile tests test_functional.py 13 | python3 -m nose 14 | python3 -m flake8 15 | --------------------------------------------------------------------------------