pax_global_header00006660000000000000000000000064130506316210014507gustar00rootroot0000000000000052 comment=9e59c3638a1ff762a8340ce0a5545df2a87e1422 qtile-0.10.7/000077500000000000000000000000001305063162100127125ustar00rootroot00000000000000qtile-0.10.7/.coveragerc000066400000000000000000000001651305063162100150350ustar00rootroot00000000000000[run] source = libqtile [report] omit = libqtile/interactive/* libqtile/scripts/* libqtile/ffi_build.py qtile-0.10.7/.gitignore000066400000000000000000000007461305063162100147110ustar00rootroot00000000000000# Python object files *.py[cd] *.py~ *.so *.o __pycache__ MANIFEST /build /dist /env # complied cffi output libqtile/_ffi_pango.py libqtile/_ffi_xcursors.py # Files generated by setup.py qtile.egg-info/ .eggs # Vim swap files *.swp *.swo /doc /docs/_build .coverage # some people hack on macs? :-) .DS_Store # debian stuff .pybuild debian/files debian/*.substvars debian/*.log debian/tmp debian/qtile debian/python-qtile debian/python3-qtile debian/*debhelper *sublime* .tox/ .cache/ qtile-0.10.7/.travis.yml000066400000000000000000000024731305063162100150310ustar00rootroot00000000000000sudo: false dist: trusty language: python matrix: include: - python: 2.7 # these are just to make travis's UI a bit prettier env: TOXENV=py27-trollius - python: 3.3 env: TOXENV=py33-trollius - python: 3.3 env: TOXENV=py33-tulip - python: 3.4 env: TOXENV=py34 - python: 3.5 env: TOXENV=py35 - python: 3.6 env: TOXENV=py36 - python: nightly env: TOXENV=py-nightly - python: pypy-5.4.1 env: TOXENV=pypy-trollius - python: pypy3.3-5.2-alpha1 env: TOXENV=pypy-trollius - python: 3.6 env: TOXENV=packaging - python: 3.6 env: TOXENV=docs - python: 3.6 env: TOXENV=pep8 - python: 2.7 env: TOXENV=pep8 allow_failures: - python: nightly env: TOXENV=py-nightly - python: pypy3.3-5.2-alpha1 env: TOXENV=pypy-trollius cache: directories: - $HOME/.cache/pip addons: apt: packages: - x11-apps - xserver-xephyr - xvfb - graphviz # for docs install: - pip install tox - if [[ $TOXENV == py* ]]; then pip install coveralls; fi script: - tox notifications: email: false after_success: - if [[ $TOXENV == py* ]]; then coveralls; fi qtile-0.10.7/CHANGELOG000066400000000000000000000414231305063162100141300ustar00rootroot00000000000000qtile 0.10.7, released 2017-02-14: * features - new MPD widget, widget.MPD2, based on `mpd2` library - add option to ignore duplicates in prompt widget - add additional margin options to GroupBox widget - add option to ignore mouse wheel to GroupBox widget - add `watts` formatting string option to Battery widgets - add volume commands to Volume widget - add Window.focus command * bugfixes - place transient windows in the middle of their parents - fix TreeTab layout - fix CurrentLayoutIcon in Python 3 - fix xcb handling for xcffib 0.5.0 - fix bug in Screen.resize - fix Qtile.display_kb command qtile 0.10.6, released 2016-05-24: !!! qsh renamed to qshell !!! This avoids name collision with other packages * features - Test framework changed to pytest - Add `startup_complete` hook * bugfixes - Restore dynamic groups on restart - Correct placement of transient_for windows - Major bug fixes with floating window handling * file path changes (XDG Base Directory specification) - the default log file path changed from ~/.qtile.log to ~/.local/share/qtile/qtile.log - the cache directory changed from ~/.cache to ~/.cache/qtile - the prompt widget's history file changed from ~/.qtile_history to ~/.cache/qtile/prompt_history qtile 0.10.5, released 2016-03-06: !!! Python 3.2 support dropped !!! !!! GoogleCalendar widget dropped for KhalCalendar widget !!! !!! qtile-session script removed in favor of qtile script !!! * features - new Columns layout, composed of dynamic and configurable columns of windows - new iPython kernel for qsh, called iqsh, see docs for installing - new qsh command `display_kb` to show current key binding - add json interface to IPC server - add commands for resizing MonadTall main panel - wlan widget shows when you are disconnected and uses a configurable format * bugfixes - fix path handling in PromptWidget - fix KeyboardLayout widget cycling keyboard - properly guard against setting screen to too large screen index qtile 0.10.4, released 2016-01-19: !!! Config breakage !!! - positional arguments to Slice layout removed, now `side` and `width` must be passed in as keyword arguments * features - add alt coin support to BitcoinTracker widget * bugfixes - don't use six.moves assignment (fix for >=setuptools-19.3) - improved floating and fullscreen handling - support empty or non-charging secondary battery in BatteryWidget - fix GoogleCalendar widget crash qtile 0.10.3, released 2015-12-25: * features - add wmii layout - add BSD support to graph widgets * bugfixes - fix (some) fullscreen problems - update google calendar widget to latest google api - improve multiple keyboard layout support - fix displaying Systray widget on secondary monitor - fix spawn file descriptor handling in Python 3 - remove duplicate assert code in test_verticaltile.py - allow padding_{x,y} and margin_{x,y} widget attrs to be set to 0 qtile 0.10.2, released 2015-10-19: * features - add qtile-top memory monitoring - GroupBox can set visible groups - new GroupBox highlighting, line - allow window state to be hidden on WindowName widget - cmd_togroup can move to current group when None sent - added MOC playback widget - added memory usage widget - log truncation, max log size, and number of log backups configurable - add a command to change to specific layout index (lazy.to_layout_index(index)) * bugfixes - fixed memory leak in dgroups - margin fixes for MonalTall layout - improved cursor warp - remove deprecated imp for Python >= 3.3 - properly close file for NetGraph - fix MondadTall layout grow/shrink secondary panes for Python 2 - Clock widget uses datetime.now() rather than .fromtimestamp() - fix Python 3 compatibility of ThermalSensor widget - various Systray fixes, including implementing XEMBED protocol - print exception to log during loading config - fixed xmonad layout margins between main and secondary panes - clear last window name from group widgets when closed - add toggleable window border to single xmonad layout * config breakage - layouts.VerticalTile `windows` is now `clients` - layouts.VerticalTile focus_next/focus_previous now take a single argument, similar to other layouts qtile 0.10.1, released 2015-07-08: This release fixes a problem that made the PyPI package uninstallable, qtile will work with a pip install now qtile 0.10.0, released 2015-07-07: !!! Config breakage !!! - various deprecated commands have been removed: Screen.cmd_nextgroup: use cmd_next_group Screen.cmd_prevgroup: use cmd_prev_group Qtile.cmd_nextlayout: use cmd_next_layout Qtile.cmd_prevlayout: use cmd_prev_layout Qtile.cmd_to_next_screen: use cmd_next_screen Qtile.cmd_to_prev_screen: use cmd_prev_screen - Clock widget: remove fmt kwarg, use format kwarg - GmailChecker widget: remove settings parameter - Maildir widget: remove maildirPath, subFolders, and separator kwargs * Dependency updates - cffi>=1.1 is now required, along with xcffib>=0.3 and cairocffi>=0.7 (the cffi 1.0 compatible versions of each) - Care must be taken that xcffib is installed *before* cairocffi * features - add support for themed cursors using xcb-cursor if available - add CheckUpdate widget, for checking package updates, this deprecates the Pacman widget - add KeyboardKbdd widget, for changing keyboard layouts - add Cmus widget, for showing song playing in cmus - add Wallpaper widget, for showing and cycling wallpaper - add EzConfig classes allowing shortcuts to define key bindings - allow GroupBox urgent highlighting through text - Bar can be placed vertically on sides of screens (widgets must be adapted for vertical viewing) - add recognizing brightness keys * bugfixes - deprecation warnings were not printing to logs, this has been fixed - fix calculation of y property of Gap - fix focus after closing floating windows and floating windows - fix various Python 3 related int/float problems - remember screen focus across restarts - handle length 1 list passed to Drawer.set_source_rgb without raising divide by zero error - properly close files opened in Graph widget - handle _NET_WM_STATE_DEMANDS_ATTENTION as setting urgency - fix get_wm_transient_for, request WINDOW, not ATOM qtile 0.9.1, released 2015-02-13: This is primarily a unicode bugfix release for 0.9.0; there were several nits related to the python2/3 unicode conversion that were simply wrong. This release also adds license headers to each file, which is necessary for distro maintainers to package Qtile. * bugfixes - fix python2's importing of gobject - fix unicode handling in several places qtile 0.9.0, released 2015-01-20: * !!! Dependency Changes !!! New dependencies will need to be installed for qtile to work - drop xpyb for xcffib (XCB bindings) - drop py2cairo for cairocffi (Cairo bindings) - drop PyGTK for asyncio (event loop, pangocairo bindings managed internally) - qtile still depends on gobject if you want to use anything that uses dbus (e.g. the mpris widgets or the libnotify widget) * features - add Python 3 and pypy support (made possible by dependency changes) - new layout for vertical monitors - add startup_once hook, which is called exactly once per session (i.e. it is not called when qtile is restarted via lazy.restart()). This eliminates the need for the execute_once() function found in lots of user configs. - add a command for showing/hiding the bar (lazy.hide_show_bar()) - warn when a widget's dependencies cannot be imported - make qtile.log more useful via better warnings in general, including deprecation and various other warnings that were previously nonexistent - new text-polling widget super classes, which enable easy implementation of various widgets that need to poll things outside the event loop. - add man pages - large documentation update, widget/layout documentation is now autogenerated from the docstrings - new ImapWidget for checking imap mailboxes * bugfixes - change default wmname to "LG3D" (this prevents some java apps from not working out of the box) - all code passes flake8 - default log level is now WARNING - all widgets now use our config framework - windows with the "About" role float by default - got rid of a bunch of unnecessary bare except: clauses qtile 0.8.0, released 2014-08-18: * features - massive widget/layout documentation update - new widget debuginfo for use in qtile development - stack has new autosplit, fair options - matrix, ratiotile, stack, xmonad, zoomy get 'margin' option - new launchbar widget - support for matching WM_CLASS and pid in Match - add support for adding dgroups rules dynamically and via ipc - Clock supports non-system timezones - new mpris2 widget - volume widget can use emoji instead of numbers - add an 'eval' function to qsh at every object level - bar gradients support more colors - new Clipboard widget (very handy!) * bugfixes - bitcoin ticker widget switched from MtGox (dead) to btc-e - all widgets now use qtile's defaults system, so their defaults are settable globally, etc. - fix behavior when screens are cloned - all widgets use a unified polling framework - "dialog" WM_TYPEs float by default - respect xrandr --primary - use a consistent font size in the default config - default config supports mouse movements and floating - fix a bug where the bar was not redrawn correctly in some multiscreen environments - add travis-ci support and make tests vastly more robust * config breakage - libqtile.layout.Stack's `stacks` parameter is now `num_stacks` qtile 0.7.0, released 2014-03-30: * features - new disk free percentage widget - new widget to display static image - per core CPU graphs - add "screen affinity" in dynamic groups - volume widget changes volume linear-ly instead of log-ly - only draw bar when idle, vastly reducing the number of bar draws and speeding things up - new Gmail widget - Tile now supports automatically managing master windows via the `master_match` parameter. - include support for minimum height, width, size increment hints * bugfixes - don't crash on any exception in main loop - don't crash on exceptions in hooks - fix a ZeroDivisionError in CPU graph - remove a lot of duplicate and unused code - Steam windows are placed more correctly - Fixed several crashes in qsh - performance improvements for some layouts - keyboard layout widget behaves better with multiple keyboard configurations * config breakage - Tile's shuffleMatch is renamed to resetMaster qtile 0.6, released 2013-05-11: !!! Config breakage !!! This release breaks your config file in several ways: - The Textbox widget no longer takes a ``name'' positional parameter, since it was redundant; you can use the ``name'' kwarg to define it. - manager.Group (now _Group) is not used to configure groups any more; config.Group replaces it. For simple configurations (i.e. Group("a") type configs), this should be a drop in replacement. config.Group also provides many more options for showing and hiding groups, assigning windows to groups by default, etc. - The Key, Screen, Drag, and Click objects have moved from the manager module to the config module. - The Match object has moved from the dgroups module to the config module. - The addgroup hook now takes two parameters: the qtile object and the name of the group added: @hook.subscribe def addgroup_hook(qtile, name): pass - The nextgroup and prevgroup commands are now on Screen instead of Group. For most people, you should be able to just: sed -i -e 's/libqtile.manager/libqtile.config' config.py ...dgroups users will need to go to a bit more work, but hopefully configuration will be much simpler now for new users. * features - New widgets: task list, - New layout: Matrix - Added ability to drag and drop groups on GroupBox - added "next urgent window" command - added font shadowing on widgets - maildir widget supports multiple folders - new config option log_level to set logging level (any of logging.{DEBUG, INFO, WARNING, ERROR, CRITICAL}) - add option to battery widget to hide while level is above a certain amount - vastly simplify configuration of dynamic groups - MPD widget now supports lots of metadata options * bugfixes - don't crash on restart when the config has errors - save layout and selected group state on restart - varous EWMH properties implemented correctly - fix non-black systray icon backgrounds - drastically reduce the number of timeout_add calls in most widgets - restart on RandR attach events to allow for new screens - log level defaults to ERROR - default config options are no longer initialized when users define their corresponding option (preventing duplicate widgets, etc.) - don't try to load config in qsh (not used) - fix font alignment across Textbox based widgets qtile 0.5, released 2012-11-11: (Note, this is not complete! Many, many changes have gone in to 0.5, by a large number of contributors. Thanks to everyone who reported a bug or fixed one!) * features - Test framework is now nose - Documentation is now in sphinx - Several install guides for various OSes - New widgets: battery based icon, MPRIS1, canto, current layout, yahoo weather, sensors, screen brightness, notifiy, pacman, windowtabs, she, crashme, wifi. - Several improvements to old widgets (e.g. battery widget displays low battery in red, GroupBox now has a better indication of which screen has focus in multi-screen setups, improvements to Prompt, etc.) - Desktop notification service. - More sane way to handle configuration files - Promote dgroups to a first class entity in libqtile - Allow layouts to be named on an instance level, so you can: layouts = [ # a layout just for gimp layout.Slice('left', 192, name='gimp', role='gimp-toolbox', fallback=layout.Slice('right', 256, role='gimp-dock', fallback=layout.Stack(stacks=1, **border_args))) ] ... dynamic_groups = { 'gimp': {'layout': 'gimp'} } Dgroups(..., dynamic_groups, ...) - New Layout: Zoomy - Add a session manager to re-exec qtile if things go south - Support for WM_TAKE_FOCUS protocol - Basic .desktop file for support in login managers - Qsh reconnects after qtile is restarted from within it - Textbox supports pango markup - Examples moved to qtile-examples repository. * bugfixes - Fix several classes of X races in a more sane way - Minor typo fixes to most widgets - Fix several crashes when drawing systray icons too early - Create directories for qtile socket as necessary - PEP8 formatting updates (though we're not totally there yet) - All unit tests pass - Lots of bugfixes to MonadTall - Create IPC socket directory if necessary - Better error if two widgets have STRETCH length - Autofloat window classes can now be overridden - xkeysyms updated qtile-0.10.7/CONTRIBUTING.md000066400000000000000000000036451305063162100151530ustar00rootroot00000000000000# How to contribute Reporting bugs -------------- Perhaps the easiest way to contribute to Qtile is to report any bugs you run into on the [github issue tracker](https://github.com/qtile/qtile/issues). Useful bug reports are ones that get bugs fixed. A useful bug report normally has two qualities: 1. **Reproducible.** If your bug is not reproducible it will never get fixed. You should clearly mention the steps to reproduce the bug. Do not assume or skip any reproducing step. Described the issue, step-by-step, so that it is easy to reproduce and fix. 2. **Specific.** Do not write a essay about the problem. Be Specific and to the point. Try to summarize the problem in minimum words yet in effective way. Do not combine multiple problems even they seem to be similar. Write different reports for each problem. To give more information about your bug you can append logs from `~/.local/share/qtile/qtile.log` or on occasionally events you can capture bugs with `xtrace` for this have a deeper look on the documentation about [capturing an xtrace](http://qtile.readthedocs.io/en/latest/manual/hacking.html#capturing-an-xtrace) Writing code ============ To get started writing code for Qtile, check out our guide to [hacking](http://qtile.readthedocs.io/en/latest/manual/hacking.html). Submit a pull request --------------------- You've done your hacking and are ready to submit your patch to Qtile. Great! Now it's time to submit a [pull request](https://help.github.com/articles/using-pull-requests) to our [issue tracker](https://github.com/qtile/qtile/issues) on Github. Pull requests are not considered complete until they include all of the following: 1. Code: Should conform PEP8 and should pass `make lint`. 2. Unit tests: Should pass on travis 3. Documentation: Should get updated if it needed **Feel free to add your contribution (no matter how small) to the appropriate place in the CHANGELOG as well!** Thanks qtile-0.10.7/LICENSE000066400000000000000000000020671305063162100137240ustar00rootroot00000000000000Copyright (c) 2008, Aldo Cortesi. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. qtile-0.10.7/MANIFEST.in000066400000000000000000000006641305063162100144560ustar00rootroot00000000000000include CHANGELOG include LICENSE include README.rst include MANIFEST.in include CONTRIBUTING.md exclude .coveragerc exclude tox.ini exclude requirements.in exclude requirements.txt exclude requirements-dev.txt exclude libqtile/_ffi*.py exclude Makefile graft libqtile/resources graft resources prune bin prune docs prune scripts prune test prune rpm include bin/iqshell recursive-exclude * __pycache__ recursive-exclude * *.py[co] qtile-0.10.7/Makefile000066400000000000000000000021571305063162100143570ustar00rootroot00000000000000default: @echo "'make check'" for tests @echo "'make check-cov'" for tests with coverage @echo "'make lint'" for source code checks @echo "'make ckpatch'" to check a patch @echo "'make clean'" to clean generated files @echo "'make man'" to generate sphinx documentation @echo "'make update-requirements'" to update the requirements files .PHONY: check check: pytest --verbose .PHONY: check-cov check-cov: pytest --verbose --with-cov libqtile --cov-report term-missing .PHONY: lint lint: flake8 ./libqtile bin/q* .PHONY: ckpatch ckpatch: lint check .PHONY: clean clean: -rm -rf dist qtile.egg-info docs/_build build/ # This is a little ugly: we want to be able to have users just run # 'python setup.py install' to install qtile, but we would also like to install # the man pages. I can't figure out a way to have the 'build' target invoke the # 'build_sphinx' target as well, so we commit the man pages, since they are # used in the 'install' target. .PHONY: man man: python setup.py build_sphinx -b man cp build/sphinx/man/* resources/ .PHONY: update-requirements update-requirements: pip-compile requirements.in qtile-0.10.7/README.rst000066400000000000000000000041761305063162100144110ustar00rootroot00000000000000===== Qtile ===== |travis| |coveralls| |rtd| A full-featured, pure-Python tiling window manager ================================================== :Website: http://www.qtile.org :Source: https://github.com/qtile/qtile :Documentation: http://qtile.readthedocs.io/en/latest/ :License: MIT License Features ======== * Simple, small and extensible. It's easy to write your own layouts, widgets and commands. * Configured in Python. * Command shell that allows all aspects of Qtile to be managed and inspected. * Complete remote scriptability - write scripts to set up workspaces, manipulate windows, update status bar widgets and more. * Qtile's remote scriptability makes it one of the most thoroughly unit-tested window managers around. Current Release =============== The current stable version of qtile is 0.10.7, released 2017-02-14. See the `documentation `_ for installation instructions. Community ========= Qtile is supported by a dedicated group of users. If you need any help, please don't hesitate to fire off an email to our mailing list or join us on IRC. :Mailing List: http://groups.google.com/group/qtile-dev :IRC: irc://irc.oftc.net:6667/qtile Contributing ============ Please report any suggestions, feature requests, bug reports, or annoyances to the Github `issue tracker`_. There are also a few `tips & tricks`_, and `guidelines`_ for contributing in the documentation. .. _`issue tracker`: https://github.com/qtile/qtile/issues .. _`tips & tricks`: http://docs.qtile.org/en/latest/manual/hacking.html .. _`guidelines`: http://docs.qtile.org/en/latest/manual/contributing.html .. |travis| image:: https://travis-ci.org/qtile/qtile.svg?branch=develop :alt: Build Status :target: https://travis-ci.org/qtile/qtile .. |coveralls| image:: https://coveralls.io/repos/github/qtile/qtile/badge.svg?branch=develop :alt: Build Coverage :target: https://coveralls.io/github/qtile/qtile?branch=develop .. |rtd| image:: https://readthedocs.org/projects/qtile/badge/?version=latest :alt: Documentation Status :target: http://docs.qtile.org/en/latest/?badge=latest qtile-0.10.7/bin/000077500000000000000000000000001305063162100134625ustar00rootroot00000000000000qtile-0.10.7/bin/iqshell000077500000000000000000000022061305063162100150510ustar00rootroot00000000000000#!/bin/sh # Copyright (c) 2008, Aldo Cortesi. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. jupyter console --kernel qshell qtile-0.10.7/bin/qshell000077500000000000000000000025311305063162100147010ustar00rootroot00000000000000#!/usr/bin/env python # Copyright (c) 2008, Aldo Cortesi. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import os import sys this_dir = os.path.dirname(__file__) base_dir = os.path.abspath(os.path.join(this_dir, "..")) sys.path.insert(0, base_dir) if __name__ == '__main__': from libqtile.scripts import qshell qshell.main() qtile-0.10.7/bin/qtile000077500000000000000000000025761305063162100145400ustar00rootroot00000000000000#!/usr/bin/env python # # Copyright (c) 2008, Aldo Cortesi. All rights reserved. # Copyright (c) 2011, Florian Mounier # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import os import sys this_dir = os.path.dirname(__file__) base_dir = os.path.abspath(os.path.join(this_dir, "..")) sys.path.insert(0, base_dir) if __name__ == '__main__': from libqtile.scripts import qtile qtile.main() qtile-0.10.7/bin/qtile-run000077500000000000000000000004041305063162100153260ustar00rootroot00000000000000#!/usr/bin/env python import os import sys this_dir = os.path.dirname(__file__) base_dir = os.path.abspath(os.path.join(this_dir, "..")) sys.path.insert(0, base_dir) if __name__ == '__main__': from libqtile.scripts import qtile_run qtile_run.main() qtile-0.10.7/bin/qtile-top000077500000000000000000000004041305063162100153240ustar00rootroot00000000000000#!/usr/bin/env python import os import sys this_dir = os.path.dirname(__file__) base_dir = os.path.abspath(os.path.join(this_dir, "..")) sys.path.insert(0, base_dir) if __name__ == '__main__': from libqtile.scripts import qtile_top qtile_top.main() qtile-0.10.7/docs/000077500000000000000000000000001305063162100136425ustar00rootroot00000000000000qtile-0.10.7/docs/BUILDING000066400000000000000000000007331305063162100147650ustar00rootroot00000000000000To build in a virtual environment (requires the 'virtualenv', 'pip' and 'graphiz' tools installed in your distro): $ cd ./docs $ virtualenv qtile $ source qtile/bin/activate $ pip install -r requirements.txt $ make html $ deactivate =============================================================================== To build on Ubuntu: $ sudo apt-get install python-sphinxcontrib.seqdiag graphiz Install: https://github.com/:snide/sphinx_rtd_theme $ cd ./docs $ make html qtile-0.10.7/docs/Makefile000066400000000000000000000133341305063162100153060ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build -E -W PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext coverage help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Qtile.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Qtile.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/Qtile" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Qtile" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." qtile-0.10.7/docs/_static/000077500000000000000000000000001305063162100152705ustar00rootroot00000000000000qtile-0.10.7/docs/_static/diagrams/000077500000000000000000000000001305063162100170575ustar00rootroot00000000000000qtile-0.10.7/docs/_static/diagrams/git-branching-strategy.diag000066400000000000000000000011361305063162100242620ustar00rootroot00000000000000seqdiag { activation = none; default_fontsize = 14; default_note_color = Yellow; feature [color=Cornsilk, label="feature"]; develop [color=PowderBlue]; master [color=LightGreen]; develop --> feature [label="git branch"]; feature -> feature [label="git commit"]; feature -> feature [label="git commit"]; develop -> feature [label="git merge", leftnote="sync with develop"]; feature -> feature [label="git commit"]; feature -> develop [label="git merge"]; === rinse and repeat === develop -> master [label="git merge", rightnote="new release!"]; } qtile-0.10.7/docs/_static/diagrams/object-graph-orig.dot000066400000000000000000000021701305063162100230720ustar00rootroot00000000000000digraph G { layout = circo; root = "root"; splines = true; node [style="filled", color=DarkGray, fillcolor=Gray, label="root"]; root; node [style="filled", color=Red, fillcolor=Tomato, label="bar"]; bar; node [style="filled", color=OrangeRed, fillcolor=Orange, label="group"]; group; node [style="filled", color=Goldenrod, fillcolor=Gold, label="layout"] layout; node [style="filled", color=DarkGreen, fillcolor=LimeGreen, label="screen"]; screen; node [style="filled", color=Blue, fillcolor=LightBlue, label="widget"]; widget; node [style="filled", color=Purple, fillcolor=Violet, label="window"]; window; root -> bar; root -> group; root -> layout; root -> screen; root -> widget; root -> window; bar -> screen; group -> layout; group -> screen; group -> window; layout -> group; layout -> screen; layout -> window; screen -> bar; screen -> layout; screen -> window; widget -> bar; widget -> group; widget -> screen; window -> group; window -> screen; window -> layout; } qtile-0.10.7/docs/_static/diagrams/object-graph.dot000066400000000000000000000023711305063162100221370ustar00rootroot00000000000000digraph G { layout = dot; splines = true; node [style="filled", color=DarkGray, fillcolor=Gray, label="root"]; root; node [style="filled", color=Red, fillcolor=Tomato, label="bar"]; bar; bar4; bar5 node [style="filled", color=OrangeRed, fillcolor=Orange, label="group"]; group; group3; group5; group6; node [style="filled", color=Goldenrod, fillcolor=Gold, label="layout"] layout; layout2; layout4; layout6; node [style="filled", color=DarkGreen, fillcolor=LimeGreen, label="screen"]; screen; screen1; screen2; screen3; screen5; screen6; node [style="filled", color=Blue, fillcolor=LightBlue, label="widget"]; widget; node [style="filled", color=Purple, fillcolor=Violet, label="window"]; window; window2; window3; window4; root -> bar; root -> group; root -> layout; root -> screen; root -> widget; root -> window; bar -> screen1; group -> layout2; group -> screen2; group -> window2; layout -> group3; layout -> screen3; layout -> window3; screen -> bar4; screen -> layout4; screen -> window4; widget -> bar5; widget -> group5; widget -> screen5; window -> group6; window -> screen6; window -> layout6; } qtile-0.10.7/docs/_static/favicon.ico000066400000000000000000000021761305063162100174170ustar00rootroot00000000000000 h(    3"U"w"3fwwD"wD"fw3w"f"ÏLJLJÏqtile-0.10.7/docs/_static/github-ribbon.png000066400000000000000000000167541305063162100205460ustar00rootroot00000000000000PNG  IHDRQOtEXtSoftwareAdobe ImageReadyqe<IDATx] X>"*بU4MH4m%m ]o O$m{ 7m&1I4hi\QQ`PD\aea<̰0|gyLYʷ~!!UqPM} mFb3&̠D}5~XyݛR%]||V]m,_EAœRgN|GUѥK>F EwFɏk=ItXj `y ܦMurEG22yr-7Zb͝<& I iK,W|CV@: PzF +铚O>$#"(qr"½U.=E5tuK%jw9n뫗|9-// OWoq"nELpzp>M 6/ n$e>qFv\ Gh)4/j( aXuZ a}k~tez{tOj(Cs:Y78z"͟2O쯢w,G X}RPTje=U~rXv&NC}_rJNpt1_jpʻPl7e/|w_rJO)u攙zZc9 `yi孔Kgu k@zxdއF5P--"x#ƪ>^mRt~vM0)X`KTn3VPxAI7lƷ^v'@6TE.ǁDEr6=N))ԫo.^kDG9ashM⚘EerF0XH7 @ )t #۠ۼ3 xú9k_9XUaiE jhm2ScAAݟ-z >lܴw2\$fVf [nбHz~"M3Vش0?z^+S#2dPRde%j( lEXUm[ۄ+Syjў2~x3NL}99 dH3o5]2~6@evT0u6PP1X֔[L ƺzC8j1M#{V6.OT˙d*=QJK3(kUV6M Fw'-jf*O0UWѩSô{'=[wPI*>\L9׃FQy[5hp;PG!Ѵ bm:K+ 6ִiy4;q;8zTvy-.o}dqyltSCjW_{㚨+p<c' JdB"Rft8IC_jJKVFxhz;{9{5fm2XAAA\,UL m@Nӥ|b *FO+njAy*Sz^zSi:m!x-Z4O+?uP q3 H5 _ơ7v*|UѳE239`UALc]e9ŐXɃAX~}G +V/fnasݓgm.%b`) _ r[ ͫTYWI8v~A Sy&DN`ƪob5ITfWs(b+눩MIz**l{O/wШ *,R焵d0fdQA T֧NO=f4wP%[boYt2}E<,RU".@o|=eY1h7 xky|asàr}*w ~$<`lu{KF - TS-gmJB 瓢NjNMci^<>TJc֜9<%ƒjc+"@X1+iP-z%u^Qlq"f5h}5Lcq'x/+/3}Ǚn(I.H*XT&tY]S0JL4s>T`μ_nDA.4_pj@5>v"XlJR3=1H;r+ ӠrXMg0 OqpYMEUYID9r,!>1,]3z՚rX+¯ltc:v,.w9<[$eťHNk”71XH >Sy2 ! RݐXAfoKws97Lڰs}KVVwD1DASyG{ktARy99(Vgq_;g, Q~T`ᓆ;#KT]"u7l҇$}Ț EL{F6cur*Ӊ} T+!>n:Xr}Y+8w;UYkNT`{PuX0EZ8yߊ+)WX~ܘ' Т@v23([_*L*Uh-\'A3ufF?A{r>Y[~ *wU[UkCK~N,e'Q7 r%}>ŭ5v尐nk_Qcciuph􋏏Z!;< ڔsRSܘ19 'Kjwex=j88_*wc, ! E?Wn\q^-?*exX2 V@0' ް1aVQ}5Mޯݟ\!`$>s}|x3<:7e]bBc(Ȁa*O0VhDa IdiA UNvnA҂`ϓ-]lQDA1tiyl3րa*O0V]mqt״h4~x\-S2E[Tgrו"w~U.^ TƸ|+ Z] *$=r\OS&K{3Z (kvlZ>sO 8)W8.|b#hv'4)8!L9CdWkOBuA˗kT ɉn8Pp@n^* HHκXa}LyBG-5`A*~s~y҃v`ymuzЎEe P'K=y.͗LΜrg)&,<܍AZva\$ qz@a8e$NZF3Ryݵw@|ip4.ħTR Yt`ٜ E! RNB"EVڼ5j㗎sr*QA*we:n2b0~R<}9R,0/xٔ&2Kqz^h8HV9K )|]bWM}(픎y䱴bٯesU #iq0-dj0؝EErی/ JPnX, R F1ERm@CϧRb h-@i܇(ӗ -" RPK A[4X}J8y}U ^TQe>T1'c,_m7YE琎H&v+  j9s6{lz q|s#IaP3'+2&#+;^d3iNC+IQ hJk-1ؾBך) Wxԡo\(2qqquﮅm9PYQ)i[NT- "@!EFG`JN)q{ȽyCbQU4sL^ p@ z X*h>~:!:2P2`Ƀ0O(lX -Rk6,Z!TpP+5uVtoB^DA0 9X{`-徟P,fB醳2ë{?AEqf3v]F0 v1b,eAʢ $1j͹?;6pWZrN*1pwɹ..R%l`Q״cT^`sEGcofS7%l4TirȩeQ:#'IH*夣uKhu q+bc Py X_ I`*Ș}qWtDiwu]QIG!OPP@PB!ȅ# Q=nˮ0\Ί~A ]h# \ Λُ7R|xRGMNjoqPBj1̢ hg%_Vro6/3VfX :!0hVB(D<ĶWlqK {臹Bךd&+{9,A@qPuw1qzL}1X:gSQS:ghaBz`hkcqg))C=P I5DGbU#zc 2``LnٱWFOf .$*E`l; էPA<]Q*VhnC,֎ս=КJjMVrVEBBG"_!SFC9i+ [t)x\!DA~ߐ}D.fAZ|[9(=J0f 1 wȁC!Y(rQEASc]9{%t w`O kyP|s;N_XDNB;VsgY iӲLL|wL xǾg\aAwj}*V_*6X6dS yHI] G6a8& T`X?=)Eee`$j1" Z!-L5@ ս^ 6cM6ـ5X6qT<S, rQ?a'+1c@0012*ۿ0z%Jۙw(Ư6TN!#B;S cA9YkX&J Y000VhDaՌUtt*aK?]S:ȸ['Wxdۇ&!,r&Jk씱B;V߹}gQBX[ͫOS s zfĤOk}"xo3 cm[Ϸn>/^@zz:ӑL!77===bBGG&&&033 LMMѤIXYYv_0LhyF$%%!::ÇL011 ajjZjA(B__\d2"//yyy@JJ ӑ"Ѻuk888}h޼9B}aʏ%wRJIIKp%!%%Bvvvh߾=5k+++C[[ud2$%%!>>=BBBbbblSNѣzVZ)ja%w҈ǡCp1AOO]tA֭[CGGGqd2#22W^EPP`jjbĈҥ ccN}{Ł `8p >|g?@DDԩaÆテ-1 Sͱp"&&+ 0zh9S>|wFBBtiӦ|>0ΨU\\,YSNe˖:u*F===C0"¥K}v>}M6ҥK1|pQ+è1|pn8ubcc1y*gϞ8~8ݻcԨQGPP1 SΨTqq1֮]-Z 66DMqܽ{M6E^0rHp0gT&>>#Găbę3g0c yv°aøa*Gv;w`ɒ%6@~p]=#Fw}bbɝQ*"‚ 0zh| A&MR/@:tݺuÛ7oa*u3J#J1e߿~~~ꫯϣ^z\0L’;4SNŁp tޝp*4|W7`bbuH T[Q~ qEKW^F!ߴaXrg*,88 .Į]u8gϞEBBΝu8 T[j Xj|R~~>._TL0p>)22... BΝa ZL,ZzzzXdر{ ^|8}"+;̛7&L@aa!0 Xrgضm~WNd2d[.6oެX߿_2,Y|ڵK 1 SΔۖ-[о}{qRDDnݺTzdd$.]Zr0{llٲJa%w\ 닩Sr }g[1cN͛]t'O=}cƌ;7o(..֭[ _;v,8PNNN8|a#)SvViiiѻwH*%OḱciTfMpNNhт|}}>|РA>} 5mTd255K.ѻwܜ\]]I&}XfGq0 Ôk3咐kkk7qD>… 𾥝 ̚5 :u 6 ==Ǐ?:8sss%ZlDcV\ڵkC~_'X[[ѣGH$*)a%w\RSSajj:uKKK߿ hbbb pa@`` ,?G(~TΡCЪU+At:x/oذ!ׯpٳgcIIIGRRR/fff "a|Ann.Vxb$''ӧh׮燩Sѣ/XXXNooo^wޅ 0|c^x={b߾}弲fee{aʂ%w\޲oxbXv-鉋/:::%0$+VD"U`mm+Wb !..R)uYYY000(E‡m$0Lyny\ԩ4٠A ۶mX,FÆ 1x` ƍb:tpms_+-?~?J舔WEPTb)LargʥCظq?~\C?ҥK6mbccѡCס٠A  D"ׯ[[[Z~=z;0’;Sn| |}}޼y˗/c{.YSNaĈ5j~/ODxBD"Aڵࠐԩ`2bɝQ}Ş={LUcL>f}+oVH 8::cǎpqqA6m>C0U K҅c]6кukC41qDļy^Ǜ7o}xx8޼y]]]888prrbbɝQTxzz"<<W̙3}իok׮jܸqׯ_Gxx8akk WWWjabɝQT5k`հuXj+̛7 SSSNcJOOGXXBCCh :u3жm[62#HOOGJJ ^zt!33Dqnܸ0~Z+ +__!::<@BBsmuuu!QfMhkkxJFaa!|4hhڴ)lllЮ];iF!S5Ψͱcǰl2ܻw_Ǝ;qFd2x{{cܹ5CW~xx8BCCЕN:SNhԨjl\rAAAǝ;w H`ff4mְ[sss&| -- "-[znݺA___WΨKZd2>|+WĽ{iӦ]}v@KK f͂7 M)ׯƍBQQ4hΝ;:uBBF8wnܸ>'''899ɧ.s#UJNNFDDnݺ7o͛dpvvF>}0rHZJ4N0m4dggN:#1s?|hA֭1uTxzz@nnnpuuS޼y" 077ǠAлwot޽ 88ϟɓ' '''xzzj:DXrg8quxxxN:عs'BCC௿B1`ywae[\\[n!((gϞEdd$1l09NNN\șbܾ}ׯ_GHH}}}Ν;J/"CŨQйsg+T*իW@H$=3gt+dXrg3f@>}ЂIHHcpܸqRpttDvkkk|$''~*555QQQFdd$BCC[[[4 Ahh(BBB---o^޲wqqT_XhΝ;f͚a̙o4'''֭[}bհ:4XrgԦؾ};.\+Vxo677׮]Cpp0nݺ۷o#//Y&W_ 4QRff&ҐT$&&"!!=Ç >o]vE=Tz)_.ػw/ V2q}GPPP+[V-|yP__dCaa!rssQTT$?G aÆhڴ|r֭ѶmJsyBCC]Jh֬|7n\Ế{]txx~zX7ʕ+^exb<}˖-Ü9sx M;rw!Jqĉ -G///wTsss~ه>k_uM41]]] 011ݻw #""PXXzɻТE2'Ǐ16lFBBB/^yU$'Hn:Z ͚5ZhuX̿èIOO:uDiii*ɓ$\_+((Zj}WOv4p@Znݼy$I$%%oD"Џ?HeϏjԨA;w^{!۪XrgTB&ъ+ѤI!O$?K}>KUCqq1EGGӦMhȐ!TN@Խ{wZ|9]|rss;qԨQ#:w\dM<ϟ/ՁD"sǣ?$ɸ!Dm۶ wM!>/`e aɽwѣQFֹ͟?N>M#---!~ /..qQ5رcj:pQѡ'Rqq10Ē;dO>%{{{266˗/W8W@e*%GSL![[[xdbbQHDb.\Hyyy d2=z4ӕ+W( Lzzz7߰|%;4!!!dbbB-[ @FFF$ ?0nҤIbɽJOO'&>B!ӑ#G\tuu͛^f 5jК5kc+o0Jݻ7nܨܹ3Q\\*> UcaÆA(b۶mXt)<GGGGE̙3\STD"ԩS1e8vX焧sHOOD"u~戊*մ9@---R3gСC1p@UYecٲe$9CCCXZZM6}vܿ\,X'Ob_Zbɝ)8 <2 aaaJYoZ*{n|ܙR2n֬Ř1c͛믿FzzB;􄗗bccH 77޽{w|?e1e<}иqRSz,3%z*PVɓ'j7//?G%$$лwTKLihт/^B!>}Z=gWIRs~~~4f""J?ZmԼys&NH(&&Td2@}MhYhlR)e11c̜9 _iw^dffbΜ9fee8'==]1AaĉXbz--00-[TS2j(5 [XX|4Wٳd;vT#)) :PPP)obb4ŔKG$ fΜ;wbɒ%Xtiή"?~<[Q طoF___ԩSr 1:u ##Ce2_1 222гgOȑ#XlZ;!55Wk o*c05kŋᅢcؽ{7@__111-T\\/Tk-1^?=X888ٳg СCT*O?///4lP3)RSSVΝ;0a\x7o`ƍ lٲEEE%舔ڵKxUKIIZbgǎرcѮ];=zfǏ٦ g#o>L2M6-Z .111ppp@XX 5kqR?.]iӦ!66:t'OR\R=2L)ן nFpK&ҥKє)SXZlIJ/la-jC"`ƌŲe˰xJׯ\TANNNFKR$''#99!!!D:ǃ.rrrcѢEĮ]43 ^ {DZcǰdɒJj*ՕP*m۶\D999ggglٲϞ=SyUǏ1l0̟?KU܇iwwwCR 2bTZZZ:Zx._VZaj[Meeeahٲ%-[u8KUѣGƍ#""Z:^ٳ'ס0UHnn.wήT=U|>M4+W B$ȑ#G^5D^~=z\T{UL&ŋTlڴđ\sN:'Np @ܼy/_& a``DyH---\ӧOPXvmgϞ֭Ο?ϖS /_!1\(_ff&׏h׮]\S#GRV>ڧZVRiԥKU?ٙV\IQQQ$Hh޽$ hر:SRRΎիGׯ_WUj277'{{Rz^>>$cǎuHg޽{G$ i֭\Sj}YXXu:+?@:t }}}y".={PffRni Ji˖-dllLjբrss^޽{Gk֬!CCC266-[T[%w HZ"ccc|2˗/I[[o߮zYri„ dmmMbX̵Ɔ[ VUdffҒ%KHOOi޼yo>|P$!###קK{%w t%222"{{{z ɜ9s\XrWlڵkߟ͉+t;99ђ%K(!!P.++vI5D 6N>^***ӧO{իGV Ccʀ%w i& 4rH:2IOO'===ڰaf]5=zDK.%'''200}k׮*UMDF;w^zH$R/{nڵ+|222)SPPPPO.\)SPڵSnń,k|"@@?FN]p!ӻw^7K'J)44&OL0Yf4~xxF~Vbb"mذ:vH<ˆ~GW^ ڰa9::ϧ5jP߾}i˖-t-*,,[,nݺE[l}R5瓣#mڴ^x8`[jdѣG8x uHeƍc޼yX`g[]nn.Μ9#G֭[x|7]]]4o=z7|͛sjĉ?p=BHRS(֭[?YNzz:.^s!88҂=c?55jT%\R)={x[CLL `nnnݺO>իLLL*TSy^ɅaС]6N8kkkC*5k`ݺux ^?K_K:tgΜAll,^~ {gdd$@eԨQ& lٲԫW݃n)"""-[@$H^ LLLPn]@[[l PTT\իWHIIAzz:1bH$x{{cǎpttDF*0[[Ō3W_Y&!Knn.6mڄSrؙd2ƃ*C׮]1l05j8bno^;=/ub 4h~~~ "m.\@||<W^!55(((@NN$|FA$AOO077G6m`jj 333XYYiӦh߾=uֱ]:. 0VXXH&M"GK,7n$]]]JOO,T{999ti=z45i҄BGM2]Fp+YaB\L&QF/Z} . 5j0%c-J&-- CEll,;V_/_&M1T8~8N:۷oիW5kDѫW/9/yʕ+d] ~XfM˞>}: addTs! Q\\`۶me ,W"QQQpwwagguHw^y:*K&Ƒ#Gpܿ999/755E߾}1x` 4/W͛7m۶dmKDN]h~7bԩST&שS2 ߙ~ jªU7!89`M={ׯ_sRH$PT|^^;wƏO ]B6mJcƌsΕzc󢣣AT^On;gΜ2vOv_xJ$mnkժU[)/^dk׮-sLǒ;$ ͞=мy4z ۻw/ J&'ڽ{7׏JQ988O111lo%  ѣ~֮]+. I&e^fǎ ?>xs>:ܹs_:F_A((('O隘ʋ%w9rSNu8J'ɨUV4rHC,=%%Ou*tjkk=͙3ٔ4nݺGʕ+x|)C AϞ=K=Q .vmmm###~%lDff&Ν;^+..ƹs琙Y*HJJkb322/mh0]T),,pT*((Э[ETrOKK#+b1Ҍ3ʕ+rOOOO*ߊ֭[4|pM@+={8Ӟ={J,_~m>2ǒd``@:tܨgϞ\&wLFݣSΝPñf͚BW;w=5@RRmۖj׮]CbtuuI #>Oƍ+]~K@ ]XƸq˷6YŰ"2V^M|>Ǝ[-Znׯ_'t5CHY{AAٳ^arG4h ڷoZ)ڪuC^^թS|||(;;l">{ϞR>RRR>[·554h@[ll3gxiРA$+E $GwQjHOOl cɽÇo((44vIb @YYYGɝgϞX,ݻw+ 6|]__:vH/2vfɽzHHH [[[255r}rU܉<<<^6>,ѣG?ŽƌC<,YR-ӴiӨaÆTXXX󋋋͛4w\jݺG]4p@ڳgO sKUŋvԶm[JJJ:"":<H䞐@<ׯ邌 TVq $رc\ȹѡ_dggѣGiĈubI&QPP`ɽj۰a T Q^TR;Q @Ô&Od2狋#"<6m?$''СC8K$%%ݻwΆT*Żw >CݺuѴiSjժh9r}]&apqqKT_۱dR)q!\|~ӺubȐ!4hڵksy LK#>>OF>}I5k 777C)3t Fݹ)#NGp5\v xR)Qn]ԭ[Ճ% 5.((dhݺ5йsg888@(,ez &M}aᾮ]"%%ZɽuVbaĈ@RRR)@[[M4AΝ1l0@$q5Sy&5kuH q9={Pmƍܹsu8LY_*RHH͘16lHА @+WǏӣG*4x#==i˖-4n8$GC~qAGǗp/~ʔ)W$%%ѺuMa?Ν;ӏ?H?:O^eڵ_~u84dj׮JP=U6 Q-&'Oj۶-YXUiU{졁ikkʕ+dIښ-ZD*+RiƌԲeKQ|>|> 8T_* 5?7~YpaW[u$H@...*Q>$7nЀSiʕJ-++8@...lmmi$H(%%j֬"$Qv觟~xNAU( <<!K---:uDzzzt)ڪˇ:;Q۶m ݿ_u1ʣI}%JjiULL M8b1YYYɻ+m6ѩs.䮹AԤI:%&&P(.u%@{Vy](%};ԱcGp2UǏʝ܋hhڕdsNӣ;j7H={6mۖ ԭ[ @;vRlllhرJ\XrԽ{wѨ7իW1:;% NeJ\͛7-~~~ʎS';;;2227nT/_ѣݻw.+''8@԰aC hʔ)Rȑ#5z@ K!66,--aÆ5#իWTF ڴiTwr?y$ &Nʜ_|IvvvdiiYe:ϧ#FP5?,W~zyvZ˸{.-Y@a1u֍֮]A2ڶmKCUY{uܙ^zu8e`266\թNDT^=DJi0U+֭[k鲐J4c \s/\@VVV)e6nX9t)=z4YYY),#I&EjpYQOaɽJ?ǣiӦiܘoߒ!^Zr?2{l˔]{nn.9::R˖-+P0|200( MO>ad2=|֭[G]v%###95k$GGGZ`ݹsGEԧONcP+oR~HKKvu8zj244oߪ^.L&:uꐶ6nlJGMׯ4$L&QFQ >;'//-[Fbךɓ֖bG D{Tݑ*>Xr|ݻGTn] 8 IDAT:r!cccZ`"Z|fJTСC]SUEaa!988|rH`` իWO^C,M6._\Ӈ:uuJ{r)Y&9;;kBQ7n$]]]NFsܥR)R͚52)/& Um޼YTZ>$]]]:|0UߣG$L&JJ'::ٳgE)Xrd2-_x<M8R[.gJDD?#[rR?e_L쬒GgΜ]eqԺu2%42:t(mۖ0%w{I$Ѷm۸vAb.{aa!ըQ5RQ'O~? Wҡek_~8}RV={˗򽍉BT{d2DEE:Dx? p SE|/޽{}}}u]jO>?~w0aڵXp!kڵ \Ua2 k֬ȑ#9K앁.Fݻwŋի!13gΐ@ Pv4h ֡l{.ŕ~SE v]rssiȑ$hÆ \TG%>O4.[dcci>4  %СChڴ)b1zDkX|9VZiӦ "DEE&L@pp0X]\\Z "Rݻ̓PN)ӧOꊠ \ps:$!"Yh޼9pƃu8?|6߼yj ⯿Btt4~7||2w͛Ea۶mŤIHlٲ.]Ç ֣n7oƍu(r  JݻsR?l[nôiӸ&gϞ~R&Mn:ޝ՜[7%TCȒR}_АTciѦY2J(J3؋d7mڷ=?/߱.xx+n}l6m ={`@mm-:w,<^KK wA࿟o.Q P"EEE ҥKѴiSq1c`ĈHHHh///?Ejt#F+Wp}q^PP֭[K$$&&V\?8x =z>࿃tttD>]cS= < .1UUUҥK/رcPSSKqej@)ŽZ G޼ioZ;w3ƍݻSTsJYYvgggHl 6 QQQ8uV\)ْ :v… ɡ'µjՊZ+֭Chh(ݻWcĭ8Ȫ}...02$!!hժRRRO;ؤ̙3)ܹ̿pvvN;h˽uԊ[[[[;УGaa|@۶mjAb0۷oF$''uaOOO <GEj <zBTT^~M;hqӧn߾-\. #|>6mooo2P|H# +}Hhh(@VXA;Bh˽_~0`~7}АW\AFFΝK;J(_8p RSSje6ښv1sLi;wNYf$;,X!!!ϖvW_ю(QQQ{.iGaTuu5?bŊ"sݝ-@ׯGyy9oN;䖯UUUѣ~g899I2Tz)z쉳g\K.E"ؖ owÇxThjj!??_!fHOUUUjl_ F~cʕ0R(11FFF(,,DrrBW^puu.\.-[B>|v ̙]]],X@yVhh(bbbeQGyٻw/###(T~vj*)|s\̙3عs$2IGa.+~__pp"l%PQQ ?vSaAAADIIDEEoz֭6m4ĉ6)iѢ7MMMR\\L;'ITwGwN;l-;;;Ϙ9s&qCz`رhݺ59"Bnܸ?!ֿ2EWޓ555XYYÇHIIGԻo20n8'00QE$|7(ߵdիׯ/3+V'F;Hdff"$$+V匌KHHQTTL2v$6Q.]qqqx8 QvFo'OÇEM"ʰ~zyyyHHH?,Wо}{ҎPg1FFFHMMEϞ=iG*!!!ʂ(rk޽ wI-8һwoLȓ'ODuj(//'DKKhтlݺҎ%r/_$M6%P-_UUE,X@8Y~\L:ңGbggG;Jm O3"+ÇIn#VVV… D?ݝhii555NiwwwҶm[RQQA; UXYz.KݻG;Jbq#Ȃ hG{"-o֒P2tPݛxzzR),,$"Ǐ'\.tܙxxxW^Q#)EEEDCCxyyюB˗/:=zwҎ#߿?d& Eet2fΜK.=z4vލ={nnn?QRR"˃#55+ƍcѢEh֬~w<}W)nk.p8ǥ`Xz矸q[I~W`ȵOn*J$%%!22.\k=z@߾}ѯ_? ;wFNЮ];()) _^^͛w΢"dgg#++ =­[еkW 6 &M„ |yVQQ]P[VWW n:_^9ZlQL|mۢ%%%l(KBfff033ҥKHKK۷'O@ ТE 4k M4AII ZhB^~*TVV ߶m[C ? Hۓ:CEE.]J; #ٰݻwɓ'ӎ$Ο?D$$$Ўp6l؀ŋc=JL$rj [(++Cee%֬Y+++ 4-[*:v숎;SNlw૯·~ q<BSS2s5 ;wv/"-w@-[!??v${}4i:::jq8q+VPH&[>W^a崣0b{nO?~7oƜ9sйsgq1Ƃ xb^7'LLL0rHQ֪U wwwQjkkI9t ӧO9aÆ!<<QQQl\ݼy111Xz5( MEEӧiǑ;R_#""PWWMÇ)'^x{{ ݺulF߾}1i$Q͛~v#VI]].\`|DLL n߾+WҎˆݻ1|p#%% <@XXXJCzz:8rE/'lN)tĤIзo_Qbnݺښvر\.-%bR]!DܹsHIIar&;;8q_o[SF4} ::_&H2M*[nÇ?y =*D8ֿ.>PQQaeQWWWWWQdTSSS ^=BXXjŧ pqq8g9]vEpp{~2 'kkk1bnZXxx8LLL ͲM6ENN:vH+*UasveTuu5-ZCaXv-&b (L=m޼666ذa6oL;̒-_?EΝ;?Ў#KKKܿ6TVVBOOs̑"![~N۶mQ^^f>M*o3---̝;v.]###(..ҥKiGahÆ -[hGYˠ|߿PQQi]vaȑ055EJJ iGK555=ڷoO;@?#@@;Lb]m۶ M6eeHUU₵k"""cɭxnnn0__iǑI˘bڵ K,Aia!++ Æ CDD91񁭭p.#{ݡ5kЎ"Xq1l'lq ǏN; ***ppp˗/qIqd+2Xh[Iu#޽;8L#yzzBYY-jXq!Cyy9+X:=111uV\I; #jjjӎ#SXq555󃃃ha>I&_~0". '''Qd +2"((yyyǠAPVV)8{,RSS,9q֭[q82wySN0sN5 HNNfxyyaԨQ066;w';L6gW UUUa…8|06n܈իW$&&… 8<( x hGz.acc^R&++ Dtt4֬Y ;%^^^033iGad׮] zb-w){ĉ0ڴiæQvu8u(o߾￑:xc4ܥO^zю?ֿ.]<==ahh &Ўˆٶm |r" :::ą hǓ*.bccqݻv]ݻw8~8P#F@.]pqDGG \._O.ż0~x 4v3fÇĉiGb@__3f̠TlٲYYY \5B^zE1a]J˸t( /..hӦ RSSٲRӧ8r.0ʫӧOc֬Y(((!H8tc?R:t(( mǎ=z4Vإ/:w [[[Q1@AA8jkk?z\~~SI?VܥPZZΜ9Vڢ vvvXd ֯_'N@MMv,"((nnnPVf7 噉 Ν;qw)cѴ(ϟ믿FTTN:I!???lͣ#F <<JJJ=L%Xq2nBtt4V^M;B***ʦWI|۷PUU)S~ z/Vܥ7 0eQN@@_PUU(͚5 K '^K",, +Wd% sҥKY+));#6oH8b#Q`mmM;xf̘111?~iiiKJcѢEԤ .s\;Xqh߾=+!~~~;v,F|W#1o>bҥ0RbΝ9s&8|>kݖ/^믿BEEvV^^  <<>>>X|9 jjjmۖvFJp8oqQ;O>EAA󑗗|eee @fjjjxPWWкuki]v:v(3!.m ӎ"^| 333ȑ#iGbx\]]iGa(#ɓ'v_7nÆ 0iFXТE Nff]]JJJc ??c4it߿? ===k$p2GYhG"tk֬8ryɓ'ѵkWڑzի Ў#5tuu)h혘x#!!xݻ7 ѽ{wt ΝÖ-[Ю];fx077ǰa`aa###+';ePVVƢE/ IDAThGKx{{ ӧOiӦS1 O"66vFB(?Űaðe 0Ɯ3gX$o۷{۸v.]ݻwcŊh޼9FSbya-wеkW8;;cÆ ȝRԩS׏" _~ׯ9B;T{aa!BCCqq$%%Yf7n&MÇK.#Kff&.^Ƣ氲ٳѲeK䐍r*00555lήܿ&&&|2Ξ=K;"##q\vF !8{,fΜ;bٳ'0̙3Gf ;hkkcΜ9 ށʕ+ѩS'ܹsݺVTXq9ZGDGGr ,,,hGbMQ !ƌٳؿ?&L&MЎh0a@<GFq|\wJ+2OOO=FFF0"_aptt#o߾I\~bܸqptt.^(kNAHH^xȝ;w`llWptti˗/#>>k֬i}3f >}<ű9 AٳgHHH`]egg#((nnnԷd&;;-¯ CCC^{Ȑ!کK &M©SDLKp_^z!-- #???hiia޼y0 qF 4/Hmر#m&DCCClܸDzbvU"&&vP[[%KĚǾ}S:tϟÑSSSEv.Yxp8_0zhZcX]̼0`6grss1|p>|ǏL1f͚~i cС( 8۷oqݻwqIZʧZY###z ɰׯ_#00K.m" }>}zsyx}7TСCamm BBDDf͚[[[ܸq&&&x`899a̖[[[,]SLIѨ5uTDFF~ "##GЎ W^vgҤI2i$RTT$kGxzzMMM3C7o$HFFHgkkK4iBJKK !===2q#w%۶m#-Z oXyy99s @+~zrMN???9H>}Ⱦ}dԩ=zDZlI !2|A>|E?~;w͛=w1y1QVV&H*2|pȺu!H^?iӆZvD?~())ŋ9|%KeeeG!XYY >qDnU dJJJbmm-|lƍm۶ Wq%/gvv6p8d̙|>())nˋ iYYY@xx8"##qFpٷo*++tRQ/---~Çً DDDzN|L555@uucǎ_~̿;w~ﱎ;w8}t7cػ9###"55SLmڴeee\.q9dee!!! ˜1c x}(**{ODiiGΊ9QF)))ѣH AAAiGa^|)sΙ3pܹ...3gpitM6mu4iu!vJ%[/_Dv>uVE,//탫+J̙3K.q ӎPySN0_C(**Bee٥K3v킊 1m4Vd &cccܻw׮]>['Nܹs߿3fP(**B?z +"͛v3#&&111Xz5;Lю4!Cr,Xd iӦF1hР說*xo%+WxjJJ ^~ 7 Xnڶm 9O> B_1ssÊ;g!ʳsuuuHKKc(8@///^Ռj۶-%N:8pyQ(())Ms6lͺGEqqpO$ѣGǃ9LLL```}}}̟?N:!11՘2e \\\nSSSjd\@2mڴhjjׯ_ӎB@ +QRR"666jYx('N.+ъT8Bٵkܹ3CTUUN:OZ"RVV۷ PUVV3g>>> e1,<}tՋvF!pAQƁp`ooXq ?(T=~8wouW~MXj6mڄBq^AA<<>/_ƥKhGa>#??{u%%%t=z􀾾>unݺ{CHH [bٲe3gڵk[M2CR: v*JJJȴi#۷o磤(FM,,,hǐ+Pw!D|#<OwCZj%ŋEI8pp\Z簖{# 9g>}: qYXXXЎH49s潵5p@}{"o0a|bͩ(:GGGl۶ 3gάX{#x{{c֬YեGall-Zʕ+3cѴ0лwza+++<{ 111_`ggkkV!Xj gg=!""<ɓ'iG@ 6صkWsmDEEb$%%!!!/_F-p\b߾}oZ X`222plR߁|),,Ă #G4+_OOO̘1={G"^~gΜݻH;#弽ѧOmuɈƳgp%$%%˸}6ajjf͚ٳsy<4iM6郃lmm}Ł0n8I|k2ﯿ=\.Ο?SS/;ER6 *&&p8r5Q$ۤ{SN$11v׏"x!QVVfbRu$==kkkҩS'8SSS|rA^|)|NPPп)++Cȋ/ꕱ߯!vvv[[[RXXبĉiǐ0FInn.8_D^?`[nv^RRBN>M֭[GFEҪU+2qDIIEEG}Fs\ҿEYO8AMEE jjjD[[DDD伬s$!!vdŊEڑ4~Aff&QQQ!En-$448;;CCCa[OO̙3ݻܺuzVX555I]]][:wLlBJJJuNYVRRBHΝ/0|p2rH1*??3 qM^?Ņt҅m("b|>\~ر4oޜ(++ o%K'NۀODrrrȲeˈ:iٲ%YjyH!͞={FVZE455:Y|X+XqoK.ŋ͵k׈.&㈄~˗/IӦMɶmhGyeee̙3dƍdرD]] iӦe˖… buQܭBAڵkG\.0a #UUUb. UUU$,,5p\ұcG)NbŽƌCk1&$$4k֌|7իW㈌~;i۶-WYYY$,,A [:::֖ڵܼyIŖPSSC"##ԩS #dȑ$<<ҎJKKIXX={6iٲ%xdԩ$22|_Mk>}7(",][akbʕhڴ)8RM  ##)iO>2 aff+V :uWlx< 333DEEŘ={6\.oÆ àA}#==qqqx".^@Çh۶H習ڴiLLL0fQD*//HMMEhh-0.Eю"uˑ&\(&)) PWW)͛333y0ܽ{W>P\\Xؽ{7VXuuu`߿?ݻ ѥKgwnݺׯիHNNFYYtuu1l0:tǏG-ĞCXqk׮?ĩShG4XZZ!11#12 pqq&M^x^!z*jkk sssxxx믿h;@D}'O=~PUU| EXq' 8&LEdh"XXX 44[a{Euu5\\\hG8BܹD$&&ҥKx ``` O0777Is'N***•-Ǐ]]bΜ93goܸ$ڵ ***ؼy3rss{.˅}-uUUx{С+++}E=@:SI[nɓájkkd.h90SUU-[A!>$VVV"==/_FBBPXX555 2077)iǕr#,, khI[eeeaCa``uuu899&NTWW~JJJ P\\ 4k ;o?m,{=xzzo߾:u*( +++ܸqᰴAAA(**+(b',䉉HOOGmm-:u333lذfff044ږ$5o;v}:w oo_ŋ6lpп@&MЩS'9UCxx8=*DXYYyHIIA޽iGbχ/͛:ЎhܻwIII‘<E>}_cѢEѵkWq/qڵOwB-*,,Ñ B%:]ڱǷ~K;Jٳ?ƌ`hjjҎȉdee͍v/R]]-ŞyQe˖Դ笫ܹs_4d>%!7oFΝӎ`pssqAюȡ?7ooF-Cvvpn żžvZbxr* 1^ Ol۷/jkk_~$`ѢE `=f<-^/"ϟ?'***d޽D6oܠ$_?bȐ!dҤI^mm-IOO'd̙D[[[A3 UCq㘐=ࣣ Ҡ󺹹.+cC455Ew^ĉ\CcHNNq__ǺYNJBEEvzٳt.5 C 9LMMuVwlƇرcqutϟGϞ=?zIο`())YYYrso߾}3V4551|Q>u{al2b/… _|wmI&066ĉ SSS6wY`ܸq(..Ei:\\\H8;;#''烛0Ŋ;w^lڴIdII {bµFܼ`nnoy: q IDAT{ޝ[njj+V:ފhǎXt))U<\.&M-Zرc.Z0c֭h޼9iGwb(..|W"66111=ovH377… affˠ*PUUʼn'0qD\{-[l2<Su?C!C#G0k,:::Xz5VX!kBDD```p'q~%={4a+Tx{=11)))(--: C &&& C]ٳg3f >|.]ٳ޽D-кuk|7&/^XYZo۶mPQQŋiG:[pttĶmKFܹHܹGf}6ꠧ333l+a9rL>7n$:K!((_ի5Y]]}p_8OhjjM6OƎKTUUqd8_?򬪪$$$???ҥKL&M333|rArssiGU8.ƍ !lڴ:t M4!,b-w@*]~3fG||<L;#nvӷZj"ɉ |S%%%ӧѬY3DDD`̘1\,\P;K /eeeؾ};n#G cSxo:}nSSSŋ8|L4\FFƎ|W8}_Җ/_eeexyyQ,R]vK,E۷oDze˰yf4Zii)RRR<)) PWW lll`jj}˟?`ס-jkk1m4YfT\|?w}'u 0i?غu+/^,5K|}qQЎȨL$$$ 11.]|ֆ96m |_Ѯ];Diw\.puu\.۶mC(tqߵk***l2QYHLLD~hGbdDmm-^x8p իW̬A'_|`23e0eB]] ZH222pu;@@ [˱e899IE_#Fȑ#l%|{[QYY ---fff022j[BCC ,azFڤbĉ(,,D^.]Ў  00v]ZZpvvƁj*/lS =ܻwO8x8z SSSv4Taa!vZ6BYݻNNN <UUUڱs\t C a+~,eeej·~w"22SLHKK.ߚB4op8NJ@EE-5z`p\رCrrr!wE&)dqߵk5ZnTуZfq|>t邡CbÆ 033F ?+]effbĉ@˖-q)ӎ%TXXАvpŽL޺uk*oWWWL2b&*j\rEx=55U8_~033annmmmj9QWWj8{,,--QVVCCCĠcǎcgٲeعs'(2KΝ;QUU˗Kpppc/`ʕb"/;; U^j|JcѢEl`!W! +** ]]] 6vPŽ~~~pvvx3s;vDψ߻ђ:]houo>bҥ0"RRR[[[@II {ikע~~~4*vBmm[fϞ mmmQ[‘/^%'' j ppp9 $3]/555󃃃ڶmK;#wĉ6m &&ƴc}Pmm-vލvaƌ4)o[NNN[ooo[666طo%qK>yuϟCf[+ϴ0" 522BTTڵkG;G۷oE)Lq߱cD[Ctt4lق~I"eD#//HIIABBP^^ bܹ011!C[]|b޼y ZŎ>uuuXblXx1('8B6oM(زe \\\$j>OB;wb&p6UEbfϞ!Cwrбcǐ 777QF˃%ľGqq1eΗ4Qc|FsW^8s kIB 畧T;w}/#M<==acc#ՃOKKKɓQPP:ԩS0`Xn:4k֌F5rȑ# ~^qq1iժYfRW]]Yz5p8~ UUUbS?uuu$## Æ #z" dΜ9d׮]ӎKMxx8rݻ0Cttt߿?QVV&\. >SL0;@\]]iGrr@]]X[EEE={6Ο?{~۵O؞?;&a/Q \UQZۊu[7jmZ[Z-GU[qXu@QAQ*(Cd(#$S**# }9yG+;|cĈXr%Ta* "Xrݛ>}:v /bŊ7U5 ,6-[vŽ3g6~:QUU/U!pvZ!JѱcGB,_\I̒%KGY?~׮]NQSS777AOO=z4۱ҥKw&M29./ڃ2}DEEq+$$$zzzٳ' e˖͍+VСC;(((ǏacccǎɉX 6sL|Emq/))?|k֬QZCMr޽ZPSS+++cpssC^TUv9ٟ/y "^O<x{{c߾}066f9Yݸqiii5j:galڴ(jG-{II -ZmܫWb.c66/7IIIX,9uuuَD"Ο?sαSahh}}}5D"xyy‚8jG-u@Drn9s& ={h6/WB Gpwwǜ9s:WcX8p Q8oq|Ǩbbb0x`c5ɴi@Dؼy3QCDvJ֭[x!=z H$(++x<LLL XYY޽;|>:v숹sbҥMVUU9s`֭Xh-[֬1aCnnnhɵڜC(J0 íѣ:t(q8!HdZ 0fرTlFlb[n믿؎X9sŋqE$&&"++ %`kk CCChiigϞ999Çe2 RqqqӧOb̘1qKQ^9}[ll+mN?3SNlOXXz p^cǎş afi D7Em)KRbFNNZhOOOk׮֭:v} quܸqDHH 1d=ny3&&&HHHh(£G6AAA puum6mN5ͭ[p!DEE5.55ÇǓ'O`llblǒ ۷pwwg;RwٴpB277'ԫW/ $ly}ڱc1tttHWW… o|_DDiiiѨQD9UUee%D" cǒ  G]v ھ};T*e;qƑ3I$p{n!@@={:_gyf;ZSHqD4|pxdmmM˗/w*bz+))Hׯ gggھ};bk^xAǏ'G*_o> &PH:::ZnMÆ P:s ,{npC,ٳ I&GEs,bܜ(jOի4tP@tAlƑJ| ikk=t]ٳ'lْ?vD+//K.ѪUOvuE P޽iIlG #((jrׯ|Ң͛7=ͱwmv'^\\LӦM#GBN:%a޽{4i$Ծ}{ԩeeeK!233)22ON{&@@ܜhժUt%*//g;BpŝM[ne; _M6Emڴ!HT5.JEԢE *ʆ&#G9mۖ"##I_N#@@_|UWWIJKK̙3JÆ #333@:::Jo>>Q+Df"kkkb; nJZZZݝ?~\6}v@+V`;Fhtqs04ydzEBBlx@OO...e KKKrTDxx8'EbĈ~:tttm6?X u!88k$ Sᅬ/^ 99Y6&qttիW1qD <QQQ6l+Yb1D$$$ 33DbŊ ѣSblڴ /eҥKCYY=*ϟ---Ec4 U۞ٺػw/'N(e휜f*011 F- Ly8aݺuَ"""0o<@SSSS)H$BVVƏ\*Q/0|p?T޻<Ӄ?Ξ= _VVWʊybb"?~ @]SL+sG!""sv dIEEw^_~PQ1}tx<[(>5Qo+WDnn.ڨnp7nܐ'$$֭[J+ϟWWWKiT8oӦMٳَ1bddd@WWw֨7oĵk닖-[Gԫؿ?ammL*aرO'77W-11IIIx9 ___,_[Q cV>}x9lllpQQ1m40 M6E㼵aڴiXr%z行L*G[[wF^~3Fϟ#))Yynn.|>@Yc.qطuVM'悈j*,^ヽ{ؘdC\rCv؎qZCCC:uꄐ,ZpY$$$ƍH$+fΜ WWWp89ƚ5k0uTmۖ8ǏGtt4`ꫯ4޴i@Dزe Q4CDofgg gϞ2sJ8::]v`}P(D߾}accv<0 ={࣏>b;Rl۶ 3gݻw5V[n߾ ___dggCOO;|||6_ǎsZ<}fff֭RSSَx?wͶߺuKtuu1{l[ vJU0a+ILL >CTUU111c;k!JahVj۷'NTbz*<;wNcs8oXp!QԞT*ŷ~#GŋGRRFrok'ٳx)G.jݦM 8pJ*",, |lmmَ֞={aÆa`k׮}}}jɒ%َ^{Y>66]v3Dd޽eee裏kkk\z0ݻJXz5k ""x8q" $׽={m<G^:L8p(j--- ÇGnn.q! 0X֭[ann???hu8p M:Unj=zD6m";… iԻwo""ʢ-[R||<ӥmʔ)dbbBwܑS#G-;w"ϑ?h@W8TJ={ѣGEoKzI<`%*v[lm۶E㽶ʕ+:D"!ԣGJHN06u,w!9 ѣ0 E-emZ'MjeU+Ą$ q4kɽ:txSN8}t_H/\>}8 СCѳgOB 4ؼy3v-[_~%$ x=rv)**‹/^ nݺ)iDe;IIIÑ֭[#::BX*7@__g;oX-ߪU+ZXX ##=uR)ڷo)RQQhTj vk.B555/qi.b`ׯg; ?0'|ݻwy\Spuu-qXΝ;s|6   ===8paaalGSyoFRR +++p}xزe򨴿;wǃ:v숏>kA IDATHHH8Js=oꫯ C?DqM 99'n87}t~'pƟ  Ahh(/q)L4 Ϟ=CHHB!Я_?̟?ÇَQSV >4K4hJKKT899xΟ?tܙ8mZDBBngӣW'J)##~ϨG YYYњ5k(66^xBzho6mٲ() !ahh.[~Xv]-oYYYٳ';̜9S&++ }ˑHšZZZի 777B٪Wj`8pwlwYYYETT|||؎hl/..7^cO`cc#F(:((( 3f}0`@݃H$Bbb"D"6o X ssspuuE>}{:c֭'N@@@*++ٱYZ`$ ~Gpޠ^g/-Z8~8\*qyu $%%!!!AVsss!еkWY ԩ\4rh"رR;5WD+V`ҥ`Νr6`jj sss(m^N0֮]lI\\YYYQ>}(??,G4vX!k׮D۷o7oD"a5*,oe˖TZZvw-%@@-[ .I礫KmڴQ˟AgdGX`._;ve˖?P2"?/C }`hhj&K=]OHHݻQQQYVZ#/刈ٳ+6oqa7UUU֭bbb`mmvfoŊď?4@$ Y[[ӁtLN5tErvv&@@'N{khx9^ؘIGGnv4Tڼy3M0a@:uݧ\#""HOO?~vt5j׮ 233x#){UU>UTT(|>|ȭS@vEEcƌQ,ٴpB233#CCCZp!D"ŋ0駟弗tROBPU׭[GhѢE #?r-/bڻw/yxxrvv+VԹu2<}~!GZ%Snn. i~ƍH|>уONt]6Xs-۶m#mmma;J?\v}̙T]]v,RFqԪU+V|Mo4g&Խ{wZ`;vJJJ2X,Zz5 2I__FMz뢲tԩYXXPllB26yyyt &WWW"dnnN~~~=UJGs,b((((*O<ilGb2?Lhƌ # ڡÇ… 믿;wFnн{w899VVVh۶[{*#77>DVVdѾ}{:tht-))q)㏘6mZ>xH$H$홯޽{ َ+w۳g&N nv'Om۶8vycB;IRXXX?F֭2G1V/_իWq \v ݃T*|C__:::044!J3TVVB6^6md СC&eJXl-[ɓ'cƍiҘۈtHR]Vv6Ein]*_޽{7qT¾}0qDԠؿ?LMMَE={ 00ǏǮ]2GqX+uB^^l(++x<Zl ]]]XZZVVV011QXR%%%пknѥK㰪 ,5*Yp!BCCY;PT,D,9(--ń p1^^?~8YONNFUUkݷݻ\n4~1 >ٳ'qXaÆڵv,NDGvv6n޼7S\qo _ oǏ͛vf ɵ ǏP9w777ѣlGaMdd$L`߾}066f;JQTq?z(Aٳr<\qoǏcܸqáCЮ];#{VƍH$Z\ٳg{!>>lQcƍ`!!!Xd ר*NNN@JJF_9jw܁Iɺ\WRRRzͥ{yyAWWOf;`z*c,E|(Wܛ'NDtt4{IcHR|kuY˗K.ӓ8JCӧÉ'`kkv,޽{wi#uw9 "Z K,|m۶AOOXu W^;\\\TCuu5.^vڹs'N D???DFFB__X*O ^*19,RVxĉԪU+ٳ'eggCDKŅ iii+)//՜Μ9jeO?ǣ5kְY....^(19ݻǣGw}HԾzׯ |x)D"Rcۓ'O닔!::g;V"3Bt ׯ_ox n 0i$ 21c۱8bkk [[[gB/0o<<Z-{yRSRRpq}lUaÆ8qَҮ]۷oN:GuCDu>y0|ڵk XrR)-[SRUUۑ4N,SRREDDИ1cJv)_(rO{&T*Tٶmۈ Tnou׎rJ1b%&&ʾBSN(%ኻ>| ӓR)ZBvv6 :aBcһKv&X, 1cV萻;}Mdffǣ{*,bcce˖En24ٳgz~7>G`Q'(WܕH,ӌ3aZdFcU.u{.ڵNJ]v]wttɓ';uؑ$Rs)˦Md?3lGjjjjԔjjj8F.]u~ݺuJTEV+@ н{w̚5 iiiؽ{7 َQǏ(..=w$&&' -UUU Bdd$/ȞL4G`` ~'TWW6c!??hٲ%&LvqYr=mڴAtt4զ~)Μ9ݻw o߾pwwG~Я_?jՊ ooo 'O֘Qbbk+$$$o߾o|033krV΢5 999ؿ?Rqa۷k-8::_~:uF7ŰaPZZ :tlR;۷GNNNo|_YYk XYYʕ+ܞjB9oqdcc˗/cРAxqF#q`h۶m>;u &`֭HOOGaa!1bdff>CΝann] =SxyyÅ ® Ǐ/Wɿ͓'O@ )Ν;vu-?,_ n #ӣ76/_f 1鑧'-^=JEEErN^ ;vɀ?C)j7o͛o}Ee⊻ 9|0፜Jq7oYXXЋ/6fFFm߾&OLGvJSN]vݻw6K< '''b֭[rSzҥKsȑW300[G}pUȑ#!>}௿b;G e,X@{w'Oq-СC7;;;XXX`̘1FyE8;;### ͛7(y? Àa|zOaal5=0±cЧOEF尅 Ϋ id``@;qT}ѢEdffFJ.]$WUV hd:q={^Yx<1 C_5_ o]ZJ@ cǎ)8%Ms*Nܹs -- !!!Jk?ʑblܸ-Rс'<==,##"/_FTTBCCѭ[7xzzSYYqÁϢ***PYY2H$% rrr`dd===H$ٓ#CUVt GTܶm0sL 22FFFlGj~oEDD߿Oņ 6+@ll,ѦM>|vvvlGjv,С̙oF7Fyy9p9r׮]A[[0`^zA .6FVV.]8#==R:::ܹ3`ii6mmڴ /^!Q^^ #//ŝ;wpmTUUK.  򂽽=EU6CRRF WWWDEEalƍ!H0gԛ!{=$''ڵkӧv0 Caa!  OOOBvh.***p8q'ODVVLLLзo_C.]о}{߂Jؾ};,,,xDEE}].䰈y9@ hsҚ ,-+//'333ZhnϟÉa200xۘT*tڲe ?:v([շo_;w._~E!۫Zf uЁmذ َZ~z :t@?\qo:Dլ [(/^ ?RmӧO> ???hXϞ=GҢEӓtttYZZR@@EDD_6I~~>͝;ؘΝ;ljL3gs̛79pŽvuؑ:t@׮]c;Jc_(//O6T*Pbx<]VsTVV˗),, F&&&LLLLJBCCҥKrݽOY($$ ‚֮]gϞ=,,,Ȉ/_NeeelWܛ"CCC:tqT{UUk׎fΜ9 FȈ.]y% ]v6l@~!YYYwww/bbbӧr3--Mg҇"KKKjѢ:*(++P266&+++f;-iԩ0 -[1.[l!mmmzlwRarrr޽{k.:ulO>C999"x&|YӜ &he낂 }+7@ <{h ewXL4uTX'N ===@cǎJ#iJZZZlll(006oL7nܨe-,,(**Qښ:t@'OlƆlll())8:p]͜;wLMMgϞM:Q7, wRk(TJ|8""HV^^NϟzȈPVhĈzj}S$/ //zL}iǎCT\\,<}J믿\qWCYYYԥKj۶-ƲG%(K$ԩM8Qs5FYY 2!cccDlGjXLIIINԶm[Y_M'ORz+x<7o[uxx8x233>WCݹsڵkG P׮]ѣGlGRLھ};M8|#a,//շnݚvYen§Rm?x<؎1DB .$ahܹj{jGEp]ٳhȐ!r}9QFq߿?xݿU4TJ/&aَ͛oo7,a K.\폠/_NFFFvuIII5j ptܙHJpDwy;wFTTBh25 .\@-p)ӇX155ӧO޽{#..ZZZrN^cǎݻw>v$%CDj B'Od;Z9|0_pm899… ѣ2PB~ IDAT224Yص-=0@駟bȐ!055ņ 䒣D.*a?ɓ'ظq#q4UXZZ… :u*|}}j*̟?XaCn؎h;UUU2e 6oެ=דdaaaGGGtppp-lmmx 믿B(6z4ܹgΜ>ϣʌO>cǎE۶mَX)aիӄ TryGGG0K*_|!cV󨒴4:y$eeeX,{,YB]t#4rHHMC*3}rHi kcǎ 5&M.JŅ%%%ԿbLMM)%%<]UU7T,{bb"3F.cmݺ,,,J.q8O>HNNf;Rsq$''z=##tzLٓ<̙3x)Ǝ+8 ???TWWe1c ??ΝxኻsrrBBB鉽{YYl ޽{2УGO?EBBLMMYɢN.\+l 443f@=˃/' Ν gggdggJOOG`` Ν#FgϞaܹ`Fܹs ,}EJJ &N&}-ZW^pB4ۗ8͛Ghj pYĉ:n}H$ߗ@ HgPg4o<\{߻Q=d#mmm5ʕ+kE-[x")SDV,2MM+_*88Bw kb׮]X~= bc˗cСJٳgBxx8̐@fPw>Bŵ033͛7e777G`` N<۷o˾~I8|h߾=\]]s `ٲe(--ҥKΧ% zv퐛y8;ŋq\]]q-#gB$)^{zz:!  ݻ+5&(,,D֭:ǢEp1<;v@VVjjjjf޼y`kk }}}kN:ʁ%qi~133C~~>4W9۷/^ 333BİI億o w'O0k,\|-[T""lذf͂7^yM.]_~%%%k/^x6667 wN,,,py`ԨQ q;gՕ+Wv.H0g|)֭T 333(tK"<<[lk_7|cΝW[XX ##R۷WH*((@6mX͠y-l۶ ֭CHHP^^v0~q^G<~HH yGEqq1<<<~zmPJׯ_Ν;H$unݻ7VXݻǫc{ذaxQDL˾0 ***d/--{k)'OSaau]v%---ڽ{7>}.\HA9YYYQ^^uo/4qΩlѣnݚΟ?v*(( >i˖- {РAԿH"7,a;F|>_.Twܡ9sjݺ5ر***hΝdhhHCv"---8p YYY4j(YfɓÇ^#ΎiرT\\L7ȊƍIWW:uDMM8\q4Hyy9@ X5h˗/дiikkӡC6Bn6iƍlh͛7sϲ+J|rx4ydlyxx( ? Ņ!KKKT\1c.]Jά_572/,_(+F;r;=z8Nŝ޽{4H$"tMMM%SSSbH GS>|-kkkHĉʑ@ȑ#.)((4j|Njժ^OQW^J/͑T*ѣGS>}Ԯ?Esw\Ђ ͝;W[*d\\2UVV+?=ڠq|}}յQ***ޞ1ݿY߫W/bڵkGYYYSޯ#aߟ.\@FFFGPy!!!dbbB7od;;GCzzz{QQQ^ZM R/O5jlNܽ{VXAvvv]d Ð %''8x |Y$m˖-(::(;G]vdeeh@4ܹs.5#G$F-^)&/o.gtI2e 9::ZT.uִs:^~xyf>j۰ax<َs"##|֜5y!SSSgߛJ 4LŋdkkgCBB<&UO<7Ґ!CM6.R޽/WRNNNZZZh޼yT\\"""_MDIRuW_}E<6mvοpŝ#wRk]p:t/^(%ڶm۠ԳgF]ϛ7Ε/nLLLd9#H诿/\\\jxdiiI#F[H{Q3y//#_IWWփuVTTDޤGlW9rjժz] VJ\~6x˗/έ~R:|0?j,ikk#ѹseYXX4$viG%+++СSɑzwKc.]jRw*dffǏ|TWWٳgׯcǎ022>tttЦMim۶䄖-[" HMMu/^K.xk|[pp0INNbbb epvvƐ!CѱQs,^aaaXx1+7:oaa!̙{bкuF`޼yؽ{7ƍjՊX:Q/III҂X,~ky<ƍtu쪪*$''#..~:oH$Zm۶F˖-;v=Gbb"cY;L333X[[k׮-ZѣGxy>:HNNÇqYܺu /^A_۶m1tPar bҤIoX[ƞ={0vXL>X`Ν CCC9UXv-֬Y=zÆ c; 3wB\~tH$WzG L6 usm=zǎCll,`cc~[npttd}Ç@ff&nܸWwAUǟ-VBVEEE.% [pE C6mZ]0qsYml0]/[ڸfd[Ғ r$VB(x\.p󋥋~0_y}gܾn$22@,-{?2u^`mmNcСMhh(}|hll$//T 8www!$${imme֭$''ckkKbb">,'W\wat:VZEBBBx|=^QSSj@@(jaac(=F;wNMJJ2o.YDݿm,[kkzQu͚5ĉUEQ_]xclڪ:99WTWWWuŊj^^=1Z׫_}m65,,l@4h뫮YF---V1ʕ+UN]l_;-P_xu+چV lRmU__ڵK SzL]ڷobmjii通 ^[[n߾]UCݸqѣ3F}Բ2sGw---jFF/&MaСCՙ3goZUUeEKKuVɓ'_Ժ:sG^͛7>>>*9Rݶm,tlZZZOXn555(Btt4K,!<< sGAٳ~V^}UyFat̙3Ivv6ϟ7vE(3AAADGGcb棪*{|G\z???f͚EDD>>>h4d?~t:#66[L*),TU>`ڵhZbccgۛ;-9p'N`Ϟ=\t+V/9~8S\\իWQUUh4xxx?N``駟Fuu5cǎݜ={'Or1)--ڵk5H"""xǰ1g .n ,YBII qqq[777s3NY~=$''SO;-zq6Bzz:_~%:F`ʊq1k,ON@@7m&//)//kkk~_0j(\]]NNN 4[[[>`LNGGGׯ_:hllӧOSSSNҒq?AAA&9 )lڴ^{I&׿';]tkײk.̙om\vy#333g`0???""" ׷ϳwkooӧOj444PWWg\888䄣#Æ cxzz2j(F-#RmHLL %%%ObʕOrrr>>Ϳox㍟t^ɓ撛Kvv6.]BQE`0@PPaaa_l}¢NiooNlllaРA.ݗ_~ITTVVV2vXsGm]ҥKIMMeǎ,]j߿;6)(( ++cǎqu,,,0 Ù6mӦM#00#Gr>!#+ԉ~UTTĬYc:֮]{St:6oLRR.\յ 呗GFF ,--F0~xBCC !((WBY~s1f͚O< Ea͚51Eaii+פsZVQ>s\]]p999dggٳgQF^___BCC O q_={ɓ'3sL{サf<޽ŋjHKK¢xzzH}}}Cb|>aw%]\GGAAA<"%ߎ;Xz5ׯ_gÆ $''nsc777OԩS v݂b.L_?󧊏C|rnj!CPUU%B%]ɓ'!''sn~ܹm.\`ذaLq7 ¤/_N\\qAznmm}S]PZZw0.Lѣs9裏Jtt4TUUQQQ3g+)8%nlܸќхwa2111XXX[??dɒ%\xF3---TWW9uDFFLwa/_ɉCa8w N6m7-]wc!_.L"77FCHH`ٲe<3EQx衇:u*gΜy93+**%11_W̞= ILL4!PUUŪUz=yyywc>|cee?eee}GkkkBBB8r-_'G0l|}}MԪU(**⭷"-- fXvv6?a֭?~LϟoVSSCPP˖-c˖-#(>*...xyyqp dJͶBa R܅I|SN8Ayy9z*^z饛˴:ƕ+Wnm?Սk׮-!N<`mIIItuu~zFşgϟS:;;SYY= ~>g~:Gs[/_0!ĽK0|IIIk###yw(//gܸqʩS;w% ]HWW...lذg}?c1"??pt:]?f)BTT0yuuu駟2c /7{@aVVV,];w(,--{|˶///|@{n<<< 5w!]D0{'O続bӦMSYYɾ}w5AڛC}]kBwa2nnn,_+V`0Vzz:g&)) ooo)//'55'(%!!f-[PSSc3I7B| jmmӓV\i8VEEf8Bwar1 .ȑ#;΀sU8qO/?F|0bs0Iq.EWWԐ#iXB{wqYZZ?*Xj?0:MMMwKz=?MJJ | eee 23f)SƦ3QXXHVVո0{l͛7,!mRŀjIKK#33BZ-֌3OOOϰapttdذa75:]ŋTVVRQQAEEOHH?8&MׅwqǹpŜ8qXkjjts  C A`ooOkk+Bwwwixnnn///7nO^z.^HCC:fcA\{ G W_fi{u/`N1ڊ 4/]I{-hCYv wYq G!6Ut\Μ xEY׶wYɵ j"pkoM 'VDAʆaDtP/Y,?ܰ:n@&(bAdf| 碥h&,sYDNCu?ch:+u!%]+&7kQmix9C$S薬Kܬ>XYzr_on %OnQ{ۢ[ngc٥jPy_ϊƴ|ץ)P8hEϜHOI <}AR ̞_:?Θzꪱ!Sy让?a9*7Zzka7pebE?\ 8h^o[`[g#VX# lp_2}p~Zo~2[&@"%˪eb jvO+~UjutUS7yx=A-c~DC([6JQ{ґ+tVRwR!8j9ktٸi-4}+puLӗ@d/c4`KkGĠm|#+!pKUY;YqMz6MvZ1D| (l~CSaP:;Rqz[-1[Lpbm;v^bVDq7L,s,dsuPz$Pj8EeQZ_Ep*1ko;l~D!%VUZP8hXrY JHchڶD%24i6s35ڳZXn Ο)w/OY$$$HdIȒȒ %/&HIENDB`qtile-0.10.7/docs/_static/widgets/graph.png000066400000000000000000000140761305063162100205550ustar00rootroot00000000000000PNG  IHDR)w=P1sRGBbKGD pHYs  tIME (^_tEXtCommentCreated with GIMPWIDATxy}_wـaaAHK4-X6j4ĶYsg>dn"^@.W 6JJO=Dyjƒɹ7'SOƨ۲܈,QlhhK^wMX6GL%֔)X S0' l{޻"l}R 2sI^z"!x[%?RĐnZ= 0 ʛu3H6 C2 "]pۥe=O$.(έ^d۔ u-oe;sIO⮸ aP0$l%ܿ kb D-D.%2eDd,Jv?W^+1'LHHĩoʛT-9f鄝»|s!s 2P~9~ Ι+.&lEi#o[9"nOI~zD: @CIEعo=,l9\칋1ƪoĬQY3H  !Jd޿cDpUUTc/<jF#Sd,a>^ހrQs4RWJ##Q.BDBL_LvRk|hxՔ=̉X MX س102 <)s("udnc5ғ{_$yJ?6;Aj$lGwὰA5&|HQ~)~׷eHx ep@T@CK/ }]8gOJXd4zBu/anfNA涯`TՂq%&Vށ{!/Kwرݴ9`56a(`+z۲`+X39#} 'Aw:?z! eH )'->B$_AbJ|wRSN`5ud?M\3hWo/ڨp|DA8äyzi0@eo};_:9k$Ş84-Q=Y)! %5a>r! U4BẌLJ"R=x6"zb3 slゔȲ> K~yΙ+HUWgjGXu@ R͏P)C~I2XBvOk<R 9DT6zg)ozQ=܃*vR |ܽ@t{@`ԕ,2/ / "/盄-ǖN?ĊIZ-@$Hy"öq;a-}`ϫ{wc,1e&֔0j&aTTD OFNA>hkAdU㤯̨rm$Hc:%[#{G1!-('zFĊp\Cb? #]E/P[ !GTʓx;2D=-<\BfEJ_;)^i)U_!fd߹#Q՟4#l{Ci^M˰O[doDر !q#d (D@JPZ/eS^J!g)m~ғ۲kbq|?֌yVO/!X&a7-Q-7$2OTGr}!VKw\DOcNGzBnJ{o0 yXq5BG 4g_L7)E -)?k{5c.[HP;8|ʞY*bd*\,l9.?G=[2qs!Vcw[q1-HW(#ĈJ `G0~5gVwo9D}(Β 3 n=$?wS~z5?MbZSWuww)Iө.RsSkC%v"'`Z,"}os6ebTMQSQ[pY5'0.T*n=RJ$ؽ s<"-!-^5VB<>Z˯T-%2P3qT0.`-s7=NUHW"Yisp^Q[9r%[aD]T"UL#)DBS.!R\u A dqr R];[t"SIa͜xfq"pNbY#K|f`MYp|?Q>ƾ*b"h;2V|泰/VrDIԱ_9AX"]~5B@J*jH%\jKO=DGU{ ?o ^ c@ W&ܯr~ct)2tVHgs\0dEҺ,+oY ,UZJ/Vp.a/8G,UMVo$lۇ,($yc͘pA" Y>Ž(-oz OCma]=,̺CAlx=||`+ִ5uJXrzG!H_{'΢!v ec7%.8ަ')dV|27}pHW4$Ò^O=?p9q֬fY 5D]턭Zv_jl=jd#+Ia@kD݈D#Sp8DJue!i$/EbTT+(ƶƕϵRZ]d5 =Tg' JT 2 …c!):) )m#ޫ'!jhƚ9 n:"HebNIU-Q8֔X=fbύ g;9D:$߾QtFXOÜX1i*阓bN(Y*!0AO#,K9<Jկ۽ nT*@T ѐ3RK#R,}&)YދOQ~aXel=Beۉ3{T[4Q@H1ȀrH8žN= vg[_8vjE,,Szxl7"iuuv#Ko&D!n*V}#+pV`#B=šޢ%UAO,Xp⒰<,P ϵD#Km$Pp @*VX;,y~+>*'aRq_ыGr}ʎc"q/ǚ٤lp -f|bXAga>6Gͥ#څa>̉TeːWiŷ"Ga֙ q~` !mi֓!롼WG ê5?vR%5zĂ)K73"s'>[mOש N#Jx|&1aNhYv1UBرj9'ԆpߔGJ@8,J, (s4\b#%zWF̝0-7|i dcTP|ӫ1'%+@Id4meܹ9q_ |G40N*H uQoJN;7bN8J .a)Ub7 !zm$>Uptǐ7_ـڋH|^"ݴnRd1'z/ ;TK'~vrn= u+oAag+'~}oX:VV5gEC࿼K΢$?t#FEQpXL]2ѭ;}wt)~s>9iZD2Z\Ra-;~is0ƞ>cb=7)Yc"$VT٧cN:DM [Cǐ\Up8gSަpo@6sŪr"tڕ^uC 7 n#FTT"5 \_7VQ:퐹X33Ta^zY̝}$XxqXj{ bCR!"%QW;۲kr.jhRT+Vr5]M_jRZz"aY8K/$"b<W Ҍo֬"?8O,ŸZ'rĩM=pnѮRIq0A@br*E}\/a_7rE~;::p\v[PhtF#j/kS'~}OiEħe,ZԄ4ZmD _֫uzQz:zxPZI!JȨՑ!_BO QrkaSE@ C!|faOѣ|ѣGeUgϦ@<|Bbɒ%;v6)//UPEam6={&|R<ˏN OKKŋdΝKzzz2Re…fsضInJ]]^ZV^ƪ̙3466t6wbl2>|HOOn{`mZx"G˳gXrOOO';;Û7ox-~ɓ'c2RӧDss3nf3'O<]vqE:::ؿ?wʕ+L0!޽{p;wP0n:eeeAk#l6PĨjg޼yPRRjܹsX,OߩT*֮]K}}}1cNWVVի=Ft4>R)y{^&T]WW739s搚JIIIPW^W3ol6YYYꢽO>!B33328C2<<\|}vff&MMMԩS1 rp88Nrssvvz=)))$''HKKjJ&?[D>#C#[<v7nׯ_҂&,5JVK nS[[Kyy9hZ4diy b$PT?~cCtԩ?|7d޿/y>LCشio/?4㉎KĬY0#plg=yݻG,$Y'B?&//`؆ HHHe1} h4l߾?ũNIIa˖-<}N'~ `p-)) ш ^gg'===X,j52i$n,>"]FYY%%%L&씅o޼ҥK)--ڵkdeeqhh,x(**ҥKTTTPRRRdݤSXX<^;wpi\.Cӱ~ߛ`X_| L&Sp_*xHLL ^6޽{'y߸q>?ٳg{aǎ~qq1;wr V+V#^f1V|2:tSNPQQ](++ K~:@cmFee tr擜 /tՔQ FGzzzpÇ?]9V+,Z(dy D1_˯mul̙'YM yjݷ^q~>%Op2#,~q?ǧeaX7CteVIpNG~~~ޯL"b4s"ќ/iM5WS(-*mmm#NE4SqP%Isq%F^!:;;Q#ܑC-IO ?&ԉDBb+_-j!D|8Έ/O_ 9b_ B@_  cN)m IENDB`qtile-0.10.7/docs/_static/widgets/systray.png000066400000000000000000000054111305063162100211630ustar00rootroot00000000000000PNG  IHDRU(IEsRGBbKGD pHYs  tIME  {RtEXtCommentCreated with GIMPW dIDATh{\u?߽==Gf@^@l!)PƐtuWvݍJ [˂[RKX(A(deY@$,L21d^=3==3}If0GT[w?stbgdAjjj*P+P+P+VZa ܼVXFoܷEOO??bSLCiM3l;3]Rā-&e[aڊB *t6{Vqq~X0"+|+Kgm˗zj3}BQV|{̌]+%VAq.3U \vre/!S7*Aڥmlt1?RTbrr\.4bqk)z(E5R\v[q\)yvEc8&lZ y.l|J냧R 9pa[|##]߱g2޽{eddBpƦkx@!ZR|s;$OlS̋*(~^ܩ4*)%pOI M $sڜԂK3@GWlOңib1RA8&pLӜqۅp 7%᭰?$ިX8 ׃{=G k8I+!k:X@&"U\ц4$C t0,hi+7}x$p8L:x] g*(mZ7zW?q#>5WhNZ. vƊMIxHa:RU .4K e{3.i $@3\tka6aD465311Aoo/|~N4%k ‚ڐ.l(Ib3H"eβK#"=N x3JÔv jhX Ofz8V#\RjݳqvocưLH2Ͽ|q@Z4'N8;Z.ًrU"p}%C(rIG ,+C\4cݯw.BwMH`QP((g^Z_;w=SuIUT9-i֨$!b/6sg4!{ZM˵J`M(.UDXRsGVuyD+ Q__͛Yf 7nD(H$`{ZaA3]?]kkYx1eaY۷o?%з}=Q]LזFT\ ԂR <cYwk{F D_2_& B8@ Q䶕@vhgiC\%|jkd5OӸrfФIYU( @S.MC#`Æ ڵɳӨ\a2sDG`PEd,WdO'Yز_y+ ^m3<'sBaGhOEx@-Mdr\Ȳ|߽$^p}|_~}8˱,mC zrQ\0\1c+ƪURhRR*='<==OU \!X9tʁ yr Y>2e_ݑmzb]A(VJJxCZk,o!h ,;OδX:8/: %l6`A(GWP< 'ǭaEsHɜ?WPddž鼴 0av|NO{!b chZpMh</NH@jд"0ToF0txI”QH_O=Az"& %+hw~#~I!I>WYMx!~uaPiYF@yC8(c[(Y̭]TKab QV~N:ZgZ5W(Lұ,92dўuܤ5ߚ6C0)撡B ! X #YOXHh=3.3 from unittest.mock import MagicMock except ImportError: from mock import MagicMock class Mock(MagicMock): # xcbq does a dir() on objects and pull stuff out of them and tries to sort # the result. MagicMock has a bunch of stuff that can't be sorted, so let's # like about dir(). def __dir__(self): return [] MOCK_MODULES = [ 'libqtile._ffi_pango', 'libqtile._ffi_xcursors', 'cairocffi', 'cffi', 'dateutil', 'dateutil.parser', 'dbus', 'dbus.mainloop.glib', 'iwlib', 'keyring', 'mpd', 'trollius', 'xcffib', 'xcffib.randr', 'xcffib.xfixes', 'xcffib.xinerama', 'xcffib.xproto', 'xdg.IconTheme', ] sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES) # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath('.')) sys.path.insert(0, os.path.abspath('../')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.autosummary', 'sphinx.ext.coverage', 'sphinx.ext.graphviz', 'sphinx.ext.todo', 'sphinx.ext.viewcode', 'sphinxcontrib.seqdiag', 'sphinx_qtile', 'numpydoc', ] numpydoc_show_class_members = False # Add any paths that contain templates here, relative to this directory. templates_path = [] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Qtile' copyright = u'2008-2016, Aldo Cortesi and contributers' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '0.10.7' # The full version, including alpha/beta/rc tags. release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build', 'man'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True # -- Options for HTML output --------fautod------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. #html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. html_favicon = '_static/favicon.ico' # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {'index': 'index.html'} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'Qtiledoc' # -- Options for LaTeX output -------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'Qtile.tex', u'Qtile Documentation', u'Aldo Cortesi', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('man/qtile', 'qtile', u'Qtile Documentation', [u'Tycho Andersen'], 1), ('man/qshell', 'qshell', u'Qtile Documentation', [u'Tycho Andersen'], 1), ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'Qtile', u'Qtile Documentation', u'Aldo Cortesi', 'Qtile', 'A hackable tiling window manager.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # only import and set the theme if we're building docs locally if not os.environ.get('READTHEDOCS'): import sphinx_rtd_theme html_theme = 'sphinx_rtd_theme' html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] graphviz_dot_args = ['-Lg'] # A workaround for the responsive tables always having annoying scrollbars. def setup(app): app.add_stylesheet("no_scrollbars.css") qtile-0.10.7/docs/index.rst000066400000000000000000000013321305063162100155020ustar00rootroot00000000000000======================================= Everything you need to know about Qtile ======================================= Getting started =============== .. toctree:: :maxdepth: 2 manual/install/index manual/config/index Commands and scripting ====================== .. toctree:: :maxdepth: 2 manual/commands/index manual/commands/scripting manual/commands/qshell manual/commands/iqshell manual/commands/qtile-top manual/commands/qtile-run Getting involved ================ .. toctree:: :maxdepth: 2 manual/contributing manual/hacking Miscellaneous ============= .. toctree:: :maxdepth: 2 manual/ref/index manual/faq manual/license * :ref:`genindex` qtile-0.10.7/docs/man/000077500000000000000000000000001305063162100144155ustar00rootroot00000000000000qtile-0.10.7/docs/man/qshell.rst000066400000000000000000000011541305063162100164400ustar00rootroot00000000000000PLACEHOLDER ----------- SYNOPSIS ======== qshell [-s qtilesocket] DESCRIPTION =========== ``qshell`` is the remote shell tool for ``qtile``. ``qshell`` supports manipulating various pieces of qtile remotely via an interactive shell. OPTIONS ======= -s qtilesocket, --socket qtilesocket Use qtilesocket for IPC communication. This can be useful if you are trying to connect to/inspect another X session's running qtile instance from your own X session (or perhaps restart qtile from a tty). BUGS ==== Bugs can be reported to the issue tracker at http://github.com/qtile/qtile. qtile-0.10.7/docs/man/qtile.rst000066400000000000000000000017651305063162100162760ustar00rootroot00000000000000PLACEHOLDER ----------- SYNOPSIS ======== qtile [-c config] [-s qtilesocket] [-l DEBUG] [-n] DESCRIPTION =========== ``qtile`` runs Qtile on the current $DISPLAY. Qtile is a tiling window manager written in python. Complete configuration information is available online at http://docs.qtile.org. OPTIONS ======= -c config, --config config Use the specified config file. -s qtilesocket, --socket qtilesocket Use qtilesocket as the IPC server. -l DEBUG, --log-level DEBUG Set the default log level, one of DEBUG, INFO, WARNING, ERROR, CRITICAL. FILES ===== Qtile searches for configuration files in the following locations: #. The location specified by the ``-c`` option. #. ``$XDG_CONFIG_HOME/qtile/config.py`` #. ``~/.config/qtile/config.py`` #. The default configuration, distributed as the python module ``libqtile.resources.default_config``. BUGS ==== Bugs can be reported to the issue tracker at http://github.com/qtile/qtile. qtile-0.10.7/docs/manual/000077500000000000000000000000001305063162100151175ustar00rootroot00000000000000qtile-0.10.7/docs/manual/commands/000077500000000000000000000000001305063162100167205ustar00rootroot00000000000000qtile-0.10.7/docs/manual/commands/index.rst000066400000000000000000000076341305063162100205730ustar00rootroot00000000000000============ Commands API ============ Qtile's command API is based on a graph of objects, where each object has a set of associated commands. The graph and object commands are used in a number of different places: * Commands can be :doc:`bound to keys ` in the Qtile configuration file. * Commands can be :doc:`called through qshell `, the Qtile shell. * The qsh can also be hooked into a Jupyter kernel :doc:`called iqshell `. * Commands can be :doc:`called from a script ` to interact with Qtile from Python. If the explanation below seems a bit complex, please take a moment to explore the API using the ``qshell`` command shell. Command lists and detailed documentation can be accessed from its built-in help command. Object Graph ============ The objects in Qtile's object graph come in seven flavours, matching the seven basic components of the window manager: ``layouts``, ``windows``, ``groups``, ``bars``, ``widgets``, ``screens``, and a special ``root`` node. Objects are addressed by a path specification that starts at the root, and follows the edges of the graph. This is what the graph looks like: .. graphviz:: /_static/diagrams/object-graph-orig.dot Each arrow can be read as "holds a reference to". So, we can see that a ``widget`` object *holds a reference to* objects of type ``bar``, ``screen`` and ``group``. Lets start with some simple examples of how the addressing works. Which particular objects we hold reference to depends on the context - for instance, widgets hold a reference to the screen that they appear on, and the bar they are attached to. Lets look at an example, starting at the root node. The following script runs the ``status`` command on the root node, which, in this case, is represented by the Client object: .. code-block:: python from libqtile.command import Client c = Client() print c.status() From the graph, we can see that the root node holds a reference to ``group`` nodes. We can access the "info" command on the current group like so: .. code-block:: python c.group.info() To access a specific group, regardless of whether or not it is current, we use the Python containment syntax. This command sends group "b" to screen 1 (by the :meth:`libqtile.config.Group.to_screen` method): .. code-block:: python c.group["b"].to_screen(1) The current ``group``, ``layout``, ``screen`` and ``window`` can be accessed by simply leaving the key specifier out. The key specifier is mandatory for ``widget`` and ``bar`` nodes. We can now drill down deeper in the graph. To access the screen currently displaying group "b", we can do this: .. code-block:: python c.group["b"].screen.info() Be aware, however, that group "b" might not currently be displayed. In that case, it has no associated screen, the path resolves to a non-existent node, and we get an exception: .. code-block:: python libqtile.command.CommandError: No object screen in path 'group['b'].screen' The graph is not a tree, since it can contain cycles. This path (redundantly) specifies the group belonging to the screen that belongs to group "b": .. code-block:: python c.group["b"].screen.group Keys ==== The key specifier for the various object types are as follows: .. list-table:: :widths: 15 30 15 40 :header-rows: 1 * - Object - Key - Optional? - Example * - bar - "top", "bottom" - No - | c.screen.bar["bottom"] * - group - Name string - Yes - | c.group["one"] | c.group * - layout - Integer index - Yes - | c.layout[2] | c.layout * - screen - Integer index - Yes - | c.screen[1] | c.screen * - widget - Widget name - No - | c.widget["textbox"] * - window - Integer window ID - Yes - | c.window[123456] | c.window qtile-0.10.7/docs/manual/commands/iqshell.rst000066400000000000000000000044741305063162100211240ustar00rootroot00000000000000======= iqshell ======= In addition to the standard ``qshell`` shell interface, we provide a kernel capable of running through Jupyter that hooks into the qshell client. The command structure and syntax is the same as qshell, so it is recommended you read that for more information about that. Dependencies ============ In order to run iqshell, you must have `ipykernel`_ and `jupyter_console`_. You can install the dependencies when you are installing qtile by running: .. code-block:: bash $ pip install qtile[ipython] Otherwise, you can just install these two packages separately, either through PyPI or through your distribution package manager. .. _ipykernel: https://pypi.python.org/pypi/ipykernel .. _jupyter_console: https://pypi.python.org/pypi/jupyter_console Installing and Running the Kernel ================================= Once you have the required dependencies, you can run the kernel right away by running: .. code-block:: bash $ python -m libqtile.interactive.iqshell_kernel However, this will merely spawn a kernel instance, you will have to run a separate frontend that connects to this kernel. A more convenient way to run the kernel is by registering the kernel with Jupyter. To register the kernel itself, run: .. code-block:: bash $ python -m libqtile.interactive.iqshell_install If you run this as a non-root user, or pass the ``--user`` flag, this will install to the user Jupyter kernel directory. You can now invoke the kernel directly when starting a Jupyter frontend, for example: .. code-block:: bash $ jupyter console --kernel qshell The ``iqshell`` script will launch a Jupyter terminal console with the qshell kernel. iqshell vs qshell ================= One of the main drawbacks of running through a Jupyter kernel is the frontend has no way to query the current node of the kernel, and as such, there is no way to set a custom prompt. In order to query your current node, you can call ``pwd``. This, however, enables many of the benefits of running in a Jupyter frontend, including being able to save, run, and re-run code cells in frontends such as the Jupyter notebook. The Jupyter kernel also enables more advanced help, text completion, and introspection capabilities (however, these are currently not implemented at a level much beyond what is available in the standard qshell). qtile-0.10.7/docs/manual/commands/qshell.rst000066400000000000000000000042751305063162100207520ustar00rootroot00000000000000====== qshell ====== The Qtile command shell is a command-line shell interface that provides access to the full complement of Qtile command functions. The shell features command name completion, and full command documentation can be accessed from the shell itself. The shell uses GNU Readline when it's available, so the interface can be configured to, for example, obey VI keybindings with an appropriate ``.inputrc`` file. See the GNU Readline documentation for more information. Navigating the Object Graph =========================== The shell presents a filesystem-like interface to the object graph - the builtin "cd" and "ls" commands act like their familiar shell counterparts: .. code-block:: bash > ls layout/ widget/ screen/ bar/ window/ group/ > cd bar bar> ls bottom/ bar> cd bottom bar['bottom']> ls screen/ bar['bottom']> cd ../.. > ls layout/ widget/ screen/ bar/ window/ group/ Note that the shell provides a "short-hand" for specifying node keys (as opposed to children). The following is a valid shell path: .. code-block:: bash > cd group/4/window/31457314 The command prompt will, however, always display the Python node path that should be used in scripts and key bindings: .. code-block:: bash group['4'].window[31457314]> Live Documentation ================== The shell ``help`` command provides the canonical documentation for the Qtile API: .. code-block:: bash > cd layout/1 layout[1]> help help command -- Help for a specific command. Builtins ======== cd exit help ls q quit Commands for this object ======================== add commands current delete doc down get info items next previous rotate shuffle_down shuffle_up toggle_split up layout[1]> help previous previous() Focus previous stack. Reference ========= Qsh --- .. autoclass:: libqtile.sh.QSh .. automethod:: libqtile.sh.QSh.do_cd .. automethod:: libqtile.sh.QSh.do_exit .. automethod:: libqtile.sh.QSh.do_ls .. automethod:: libqtile.sh.QSh.do_pwd .. automethod:: libqtile.sh.QSh.do_help qtile-0.10.7/docs/manual/commands/qtile-run.rst000066400000000000000000000004601305063162100213720ustar00rootroot00000000000000========= qtile-run ========= Run a command applying rules to the new windows, ie, you can start a window in a specific group, make it floating, intrusive, etc. The Windows must have NET_WM_PID. .. code-block:: bash # run xterm floating on group "test-group" qtile-run -g test-group -f xterm qtile-0.10.7/docs/manual/commands/qtile-top.rst000066400000000000000000000001331305063162100213650ustar00rootroot00000000000000========= qtile-top ========= Is a top like to measure memory usage of qtile's internals. qtile-0.10.7/docs/manual/commands/scripting.rst000066400000000000000000000022501305063162100214530ustar00rootroot00000000000000========= Scripting ========= Client-Server Scripting Model ============================= Qtile has a client-server control model - the main Qtile instance listens on a named pipe, over which marshalled command calls and response data is passed. This allows Qtile to be controlled fully from external scripts. Remote interaction occurs through an instance of the :class:`libqtile.command.Client` class. This class establishes a connection to the currently running instance of Qtile, and sources the user's configuration file to figure out which commands should be exposed. Commands then appear as methods with the appropriate signature on the ``Client`` object. The object hierarchy is described in the :doc:`/manual/commands/index` section of this manual. Full command documentation is available through the :doc:`Qtile Shell `. Example ======= Below is a very minimal example script that inspects the current qtile instance, and returns the integer offset of the current screen. .. code-block:: python from libqtile.command import Client c = Client() print c.screen.info()["index"] Reference ========= .. qtile_class:: libqtile.command.Client qtile-0.10.7/docs/manual/config/000077500000000000000000000000001305063162100163645ustar00rootroot00000000000000qtile-0.10.7/docs/manual/config/gnome.rst000066400000000000000000000047271305063162100202350ustar00rootroot00000000000000==================== Running Inside Gnome ==================== Add the following snippet to your Qtile configuration. As per `this page `_, it registers Qtile with gnome-session. Without it, a "Something has gone wrong!" message shows up a short while after logging in. dbus-send must be on your $PATH. .. code-block:: python import subprocess import os from libqtile import hook @hook.subscribe.startup def dbus_register(): x = os.environ['DESKTOP_AUTOSTART_ID'] subprocess.Popen(['dbus-send', '--session', '--print-reply=string', '--dest=org.gnome.SessionManager', '/org/gnome/SessionManager', 'org.gnome.SessionManager.RegisterClient', 'string:qtile', 'string:' + x]) This adds a new entry "Qtile GNOME" to GDM's login screen. :: $ cat /usr/share/xsessions/qtile_gnome.desktop [Desktop Entry] Name=Qtile GNOME Comment=Tiling window manager TryExec=/usr/bin/gnome-session Exec=gnome-session --session=qtile Type=XSession The custom session for gnome-session. :: $ cat /usr/share/gnome-session/sessions/qtile.session [GNOME Session] Name=Qtile session RequiredComponents=qtile;gnome-settings-daemon; So that Qtile starts automatically on login. :: $ cat /usr/share/applications/qtile.desktop [Desktop Entry] Type=Application Encoding=UTF-8 Name=Qtile Exec=qtile NoDisplay=true X-GNOME-WMName=Qtile X-GNOME-Autostart-Phase=WindowManager X-GNOME-Provides=windowmanager X-GNOME-Autostart-Notify=false The above does not start gnome-panel. Getting gnome-panel to work requires some extra Qtile configuration, mainly making the top and bottom panels static on panel startup and leaving a gap at the top (and bottom) for the panel window. You might want to add keybindings to log out of the GNOME session. .. code-block:: python Key([mod, 'control'], 'l', lazy.spawn('gnome-screensaver-command -l')), Key([mod, 'control'], 'q', lazy.spawn('gnome-session-quit --logout --no-prompt')), Key([mod, 'shift', 'control'], 'q', lazy.spawn('gnome-session-quit --power-off')), The above apps need to be in your path (though they are typically installed in ``/usr/bin``, so they probably are if they're installed at all). qtile-0.10.7/docs/manual/config/groups.rst000066400000000000000000000024141305063162100204360ustar00rootroot00000000000000====== Groups ====== A group is a container for a bunch of windows, analogous to workspaces in other window managers. Each client window managed by the window manager belongs to exactly one group. The ``groups`` config file variable should be initialized to a list of :class:`~libqtile.dgroups.DGroup` objects. :class:`~libqtile.dgroups.DGroup` objects provide several options for group configuration. Groups can be configured to show and hide themselves when they're not empty, spawn applications for them when they start, automatically acquire certain groups, and various other options. Example ======= :: from libqtile.config import Group, Match groups = [ Group("a"), Group("b"), Group("c", matches=[Match(wm_class=["Firefox"])]), ] # allow mod3+1 through mod3+0 to bind to groups; if you bind your groups # by hand in your config, you don't need to do this. from libqtile.dgroups import simple_key_binder dgroups_key_binder = simple_key_binder("mod3") Reference ========= .. qtile_class:: libqtile.config.Group :no-commands: .. autofunction:: libqtile.dgroups.simple_key_binder Group Matching ============== .. qtile_class:: libqtile.config.Match :no-commands: .. qtile_class:: libqtile.config.Rule :no-commands: qtile-0.10.7/docs/manual/config/hooks.rst000066400000000000000000000036141305063162100202450ustar00rootroot00000000000000===== Hooks ===== Qtile provides a mechanism for subscribing to certain events in ``libqtile.hook``. To subscribe to a hook in your configuration, simply decorate a function with the hook you wish to subscribe to. See :doc:`/manual/ref/hooks` for a listing of available hooks. Examples ======== Automatic floating dialogs -------------------------- Let's say we wanted to automatically float all dialog windows (this code is not actually necessary; Qtile floats all dialogs by default). We would subscribe to the ``client_new`` hook to tell us when a new window has opened and, if the type is "dialog", as can set the window to float. In our configuration file it would look something like this: .. code-block:: python from libqtile import hook @hook.subscribe.client_new def floating_dialogs(window): dialog = window.window.get_wm_type() == 'dialog' transient = window.window.get_wm_transient_for() if dialog or transient: window.floating = True A list of available hooks can be found in the :doc:`Built-in Hooks ` reference. Autostart --------- If you want to run commands or spawn some applications when Qtile starts, you'll want to look at the ``startup`` and ``startup_once`` hooks. ``startup`` is emitted every time Qtile starts (including restarts), whereas ``startup_once`` is only emitted on the very first startup. Let's create a file ``~/.config/qtile/autostart.sh`` that will set our desktop wallpaper and start a few programs when Qtile first runs. .. code-block:: bash #!/bin/sh feh --bg-scale ~/images/wallpaper.jpg & pidgin & dropbox start & We can then subscribe to ``startup_once`` to run this script: .. code-block:: python import os import subprocess @hook.subscribe.startup_once def autostart(): home = os.path.expanduser('~/.config/qtile/autostart.sh') subprocess.call([home]) qtile-0.10.7/docs/manual/config/index.rst000066400000000000000000000135551305063162100202360ustar00rootroot00000000000000============= Configuration ============= Qtile is configured in Python. A script (``~/.config/qtile/config.py`` by default) is evaluated, and a small set of configuration variables are pulled from its global namespace. Configuration lookup order ========================== Qtile looks in the following places for a configuration file, in order: * The location specified by the ``-c`` argument. * ``$XDG_CONFIG_HOME/qtile/config.py``, if it is set * ``~/.config/qtile/config.py`` * It reads the module ``libqtile.resources.default_config``, included by default with every Qtile installation. Default Configuration ===================== The `default configuration `_ is invoked when qtile cannot find a configuration file. In addition, if qtile is restarted via qshell, qtile will load the default configuration if the config file it finds has some kind of error in it. The documentation below describes the configuration lookup process, as well as what the key bindings are in the default config. The default config is not intended to be suitable for all users; it's mostly just there so qtile does /something/ when fired up, and so that it doesn't crash and cause you to lose all your work if you reload a bad config. Key Bindings ------------ The mod key for the default config is ``mod4``, which is typically bound to the "Super" keys, which are things like the windows key and the mac command key. The basic operation is: * ``mod + k`` or ``mod + j``: switch windows on the current stack * ``mod + ``: put focus on the other pane of the stack (when in stack layout) * ``mod + ``: switch layouts * ``mod + w``: close window * ``mod + + r``: restart qtile with new config * ``mod + ``: switch to that group * ``mod + + ``: send a window to that group * ``mod + ``: start xterm * ``mod + r``: start a little prompt in the bar so users can run arbitrary commands The default config defines one screen and 8 groups, one for each letter in ``asdfuiop``. It has a basic bottom bar that includes a group box, the current window name, a little text reminder that you're using the default config, a system tray, and a clock. The default configuration has several more advanced key combinations, but the above should be enough for basic usage of qtile. Mouse Bindings -------------- By default, holding your ``mod`` key and clicking (and holding) a window will allow you to drag it around as a floating window. Configuration variables ======================= A Qtile configuration consists of a file with a bunch of variables in it, which qtile imports and then runs as a python file to derive its final configuration. The documentation below describes the most common configuration variables; more advanced configuration can be found in the `qtile-examples `_ repository, which includes a number of real-world configurations that demonstrate how you can tune Qtile to your liking. (Feel free to issue a pull request to add your own configuration to the mix!) .. toctree:: :maxdepth: 1 lazy groups keys layouts mouse screens hooks In addition to the above variables, there are several other boolean configuration variables that control specific aspects of Qtile's behavior: .. list-table:: :widths: 10 10 80 :header-rows: 1 * - variable - default - description * - follow_mouse_focus - False - Controls whether or not focus follows the mouse around as it moves across windows in a layout. * - bring_front_click - False - When clicked, should the window be brought to the front or not. (This sets the X Stack Mode to Above.) * - cursor_warp - False - If true, the cursor follows the focus as directed by the keyboard, warping to the center of the focused window. * - auto_fullscreen - True - If a window requests to be fullscreen, it is automatically fullscreened. Set this to false if you only want windows to be fullscreen if you ask them to be. * - focus_on_window_activation - urgent - Behavior of the _NET_ACTIVATE_WINDOW message sent by applications - urgent: urgent flag is set for the window - focus: automatically focus the window - smart: automatically focus if the window is in the current group Testing your configuration ========================== The best way to test changes to your configuration is with the provided Xephyr script. This will run Qtile with your ``config.py`` inside a nested X server and prevent your running instance of Qtile from crashing if something goes wrong. See :doc:`Hacking Qtile ` for more information on using Xephyr. Starting Qtile ============== There are several ways to start Qtile. The most common way is via an entry in your X session manager's menu. The default Qtile behavior can be invoked by creating a `qtile.desktop `_ file in ``/usr/share/xsessions``. A second way to start Qtile is a custom X session. This way allows you to invoke Qtile with custom arguments, and also allows you to do any setup you want (e.g. special keyboard bindings like mapping caps lock to control, setting your desktop background, etc.) before Qtile starts. If you're using an X session manager, you still may need to create a ``custom.desktop`` file similar to the ``qtile.desktop`` file above, but with ``Exec=/etc/X11/xsession``. Then, create your own ``~/.xsession``. There are several examples of user defined ``xsession`` s in the `qtile-examples `_ repository. Finally, if you're a gnome user, you can start integrate Qtile into Gnome's session manager and use gnome as usual: .. toctree:: :maxdepth: 1 gnome qtile-0.10.7/docs/manual/config/keys.rst000066400000000000000000000052611305063162100200750ustar00rootroot00000000000000==== Keys ==== The ``keys`` variable defines Qtile's key bindings. Individual key bindings are defined with :class:`libqtile.config.Key` as demonstrated in the following example. Note that you may specify more than one callback functions. :: from libqtile.config import Key keys = [ # Pressing "Meta + Shift + a". Key(["mod4", "shift"], "a", callback, ...), # Pressing "Control + p". Key(["control"], "p", callback, ...), # Pressing "Meta + Tab". Key(["mod4", "mod1"], "Tab", callback, ...), ] The above may also be written more concisely with the help of the :class:`libqtile.config.EzKey` helper class. The following example is functionally equivalent to the above:: from libqtile.config import EzKey as Key keys = [ Key("M-S-a", callback, ...), Key("C-p", callback, ...), Key("M-A-", callback, ...), ] The :class:`EzKey` modifier keys (i.e. ``MASC``) can be overwritten through the ``EzKey.modifier_keys`` dictionary. The defaults are:: modifier_keys = { 'M': 'mod4', 'A': 'mod1', 'S': 'shift', 'C': 'control', } Modifiers ========= On most systems ``mod1`` is the Alt key - you can see which modifiers, which are enclosed in a list, map to which keys on your system by running the ``xmodmap`` command. This example binds ``Alt-k`` to the "down" command on the current layout. This command is standard on all the included layouts, and switches to the next window (where "next" is defined differently in different layouts). The matching "up" command switches to the previous window. Modifiers include: "shift", "lock", "control", "mod1", "mod2", "mod3", "mod4", and "mod5". They can be used in combination by appending more than one modifier to the list: :: Key( ["mod1", "control"], "k", lazy.layout.shuffle_down() ) Special keys ============ These are most commonly used special keys. For complete list please see `the code `_. You can create bindings on them just like for the regular keys. For example ``Key(["mod1"], "F4", lazy.window.kill())``. .. list-table:: * - ``Return`` * - ``BackSpace`` * - ``Tab`` * - ``space`` * - ``Home``, ``End`` * - ``Left``, ``Up``, ``Right``, ``Down`` * - ``F1``, ``F2``, ``F3``, ... * - * - ``XF86AudioRaiseVolume`` * - ``XF86AudioLowerVolume`` * - ``XF86AudioMute`` * - ``XF86AudioNext`` * - ``XF86AudioPrev`` * - ``XF86MonBrightnessUp`` * - ``XF86MonBrightnessDown`` Reference ========= .. qtile_class:: libqtile.config.Key :no-commands: .. qtile_class:: libqtile.config.EzConfig :no-commands: qtile-0.10.7/docs/manual/config/layouts.rst000066400000000000000000000013661305063162100206240ustar00rootroot00000000000000======= Layouts ======= A layout is an algorithm for laying out windows in a group on your screen. Since Qtile is a tiling window manager, this usually means that we try to use space as efficiently as possible, and give the user ample commands that can be bound to keys to interact with layouts. The ``layouts`` variable defines the list of layouts you will use with Qtile. The first layout in the list is the default. If you define more than one layout, you will probably also want to define key bindings to let you switch to the next and previous layouts. See :doc:`/manual/ref/layouts` for a listing of available layouts. Example ======= :: from libqtile import layout layouts = [ layout.Max(), layout.Stack(stacks=2) ] qtile-0.10.7/docs/manual/config/lazy.rst000066400000000000000000000053021305063162100200750ustar00rootroot00000000000000============ Lazy objects ============ The ``command.lazy`` object is a special helper object to specify a command for later execution. This object acts like the root of the object graph, which means that we can specify a key binding command with the same syntax used to call the command through a script or through :doc:`/manual/commands/qshell`. Example ------- :: from libqtile.config import Key from libqtile.command import lazy keys = [ Key( ["mod1"], "k", lazy.layout.down() ), Key( ["mod1"], "j", lazy.layout.up() ) ] Lazy functions ============== This is overview of the commonly used functions for the key bindings. These functions can be called from commands on the :ref:`qtile_commands` object or on another object in the command tree. Some examples are given below. General functions ----------------- .. list-table:: :widths: 20 80 :header-rows: 1 * - function - description * - ``lazy.spawn("application")`` - Run the ``application`` * - ``lazy.spawncmd()`` - Open command prompt on the bar. See prompt widget. * - ``lazy.restart()`` - Restart Qtile and reload its config. It won't close your windows * - ``lazy.shutdown()`` - Close the whole Qtile Group functions --------------- .. list-table:: :widths: 20 80 :header-rows: 1 * - function - description * - ``lazy.next_layout()`` - Use next layout on the actual group * - ``lazy.prev_layout()`` - Use previous layout on the actual group * - ``lazy.screen.next_group()`` - Move to the group on the right * - ``lazy.screen.prev_group()`` - Move to the group on the left * - ``lazy.screen.toggle_group()`` - Move to the last visited group * - ``lazy.group["group_name"].toscreen()`` - Move to the group called ``group_name`` * - ``lazy.layout.increase_ratio()`` - Increase the space for master window at the expense of slave windows * - ``lazy.layout.decrease_ratio()`` - Decrease the space for master window in the advantage of slave windows Window functions ---------------- .. list-table:: :widths: 20 80 :header-rows: 1 * - function - description * - ``lazy.window.kill()`` - Close the focused window * - ``lazy.layout.next()`` - Switch window focus to other pane(s) of stack * - ``lazy.window.togroup("group_name")`` - Move focused window to the group called ``group_name`` * - ``lazy.window.toggle_floating()`` - Put the focused window to/from floating mode * - ``lazy.window.toggle_fullscreen()`` - Put the focused window to/from fullscreen mode qtile-0.10.7/docs/manual/config/mouse.rst000066400000000000000000000022631305063162100202510ustar00rootroot00000000000000===== Mouse ===== The ``mouse`` config file variable defines a set of global mouse actions, and is a list of :class:`~libqtile.config.Click` and :class:`~libqtile.config.Drag` objects, which define what to do when a window is clicked or dragged. Example ======= :: from libqtile.config import Click, Drag mouse = [ Drag([mod], "Button1", lazy.window.set_position_floating(), start=lazy.window.get_position()), Drag([mod], "Button3", lazy.window.set_size_floating(), start=lazy.window.get_size()), Click([mod], "Button2", lazy.window.bring_to_front()) ] The above example can also be written more concisely with the help of the ``EzClick`` and ``EzDrag`` helpers:: from libqtile.config import EzClick as EzClick, EzDrag as Drag mouse = [ Drag("M-1", lazy.window.set_position_floating(), start=lazy.window.get_position()), Drag("M-3", lazy.window.set_size_floating(), start=lazy.window.get_size()), Click("M-2", lazy.window.bring_to_front()) ] Reference ========= .. qtile_class:: libqtile.config.Click :no-commands: .. qtile_class:: libqtile.config.Drag :no-commands: qtile-0.10.7/docs/manual/config/screens.rst000066400000000000000000000031341305063162100205610ustar00rootroot00000000000000======= Screens ======= The ``screens`` configuration variable is where the physical screens, their associated ``bars``, and the ``widgets`` contained within the bars are defined. See :doc:`/manual/ref/widgets` for a listing of available widgets. Example ======= Tying together screens, bars and widgets, we get something like this: :: from libqtile.config import Screen from libqtile import bar, widget screens = [ Screen( bottom=bar.Bar([ widget.GroupBox(), widget.WindowName() ], 30), ), Screen( bottom=bar.Bar([ widget.GroupBox(), widget.WindowName() ], 30), ) ] Bars support both solid background colors and gradients by supplying a list of colors that make up a linear gradient. For example, :code:`bar.Bar(..., background="#000000")` will give you a black back ground (the default), while :code:`bar.Bar(..., background=["#000000", "#FFFFFF"])` will give you a background that fades from black to white. Third-party bars ================ There might be some reasons to use third-party bars. For instance you can come from another window manager and you have already configured dzen2, xmobar, or something else. They definitely can be used with Qtile too. In fact, any additional configurations aren't needed. Just run the bar and qtile will adapt. Reference ========= .. qtile_class:: libqtile.config.Screen :no-commands: .. qtile_class:: libqtile.bar.Bar :no-commands: .. qtile_class:: libqtile.bar.Gap :no-commands: qtile-0.10.7/docs/manual/contributing.rst000066400000000000000000000047171305063162100203710ustar00rootroot00000000000000============ Contributing ============ Reporting bugs ============== Perhaps the easiest way to contribute to Qtile is to report any bugs you run into on the `github issue tracker `_. Useful bug reports are ones that get bugs fixed. A useful bug report normally has two qualities: 1. **Reproducible.** If your bug is not reproducible it will never get fixed. You should clearly mention the steps to reproduce the bug. Do not assume or skip any reproducing step. Described the issue, step-by-step, so that it is easy to reproduce and fix. 2. **Specific.** Do not write a essay about the problem. Be Specific and to the point. Try to summarize the problem in minimum words yet in effective way. Do not combine multiple problems even they seem to be similar. Write different reports for each problem. Writing code ============ To get started writing code for Qtile, check out our guide to :doc:`hacking`. Git workflow ------------ Our workflow is based on Vincent Driessen's `successful git branching model `_: * The ``master`` branch is our current release * The ``develop`` branch is what all pull requests should be based against * Feature branches are where new features, both major and minor, should be developed. .. seqdiag:: /_static/diagrams/git-branching-strategy.diag `git-flow `_ is a git plugin that helps facilitate this branching strategy. It's not required, but can help make things a bit easier to manage. There is also a good write up on `using git-flow `_. We also request that git commit messages follow the `standard format `_. Submit a pull request --------------------- You've done your hacking and are ready to submit your patch to Qtile. Great! Now it's time to submit a `pull request `_ to our `issue tracker `_ on Github. .. important:: Pull requests are not considered complete until they include all of the following: * **Code** that conforms to PEP8. * **Unit tests** that pass locally and in our CI environment. * **Documentation** updates on an as needed basis. Feel free to add your contribution (no matter how small) to the appropriate place in the CHANGELOG as well! qtile-0.10.7/docs/manual/faq.rst000066400000000000000000000062201305063162100164200ustar00rootroot00000000000000========================== Frequently Asked Questions ========================== Why the name Qtile? =================== Users often wonder, why the Q? Does it have something to do with Qt? No. Below is an IRC excerpt where cortesi explains the great trial that ultimately brought Qtile into existence, thanks to the benevolence of the Open Source Gods. Praise be to the OSG! :: ramnes: what does Qtile mean? ramnes: what's the Q? @tych0: ramnes: it doesn't :) @tych0: cortesi was just looking for the first letter that wasn't registered in a domain name with "tile" as a suffix @tych0: qtile it was :) cortesi: tych0, dx: we really should have something more compelling to explain the name. one day i was swimming at manly beach in sydney, where i lived at the time. suddenly, i saw an enormous great white right beside me. it went for my leg with massive, gaping jaws, but quick as a flash, i thumb-punched it in both eyes. when it reared back in agony, i saw that it had a jagged, gnarly scar on its stomach... a scar shaped like the letter "Q". cortesi: while it was distracted, i surfed a wave to shore. i knew that i had to dedicate my next open source project to the ocean gods, in thanks for my lucky escape. and thus, qtile got its name... When I first start xterm/urxvt/rxvt containing an instance of Vim, I see text and layout corruption. What gives? ================================================================================================================ Vim is not handling terminal resizes correctly. You can fix the problem by starting your xterm with the "-wf" option, like so: .. code-block:: bash xterm -wf -e vim Alternatively, you can just cycle through your layouts a few times, which usually seems to fix it. How do I know which modifier specification maps to which key? ============================================================= To see a list of modifier names and their matching keys, use the ``xmodmap`` command. On my system, the output looks like this: .. code-block:: bash $ xmodmap xmodmap: up to 3 keys per modifier, (keycodes in parentheses): shift Shift_L (0x32), Shift_R (0x3e) lock Caps_Lock (0x9) control Control_L (0x25), Control_R (0x69) mod1 Alt_L (0x40), Alt_R (0x6c), Meta_L (0xcd) mod2 Num_Lock (0x4d) mod3 mod4 Super_L (0xce), Hyper_L (0xcf) mod5 ISO_Level3_Shift (0x5c), Mode_switch (0xcb) My "pointer mouse cursor" isn't the one I expect it to be! ========================================================== Qtile should set the default cursor to left_ptr, you must install xcb-util-cursor if you want support for themed cursors. LibreOffice menus don't appear or don't stay visible ==================================================== A workaround for problem with the mouse in libreoffice is setting the environment variable »SAL_USE_VCLPLUGIN=gen«. It is dependet on your system configuration where to do this. e.g. ArchLinux with libreoffice-fresh in /etc/profile.d/libreoffice-fresh.sh. qtile-0.10.7/docs/manual/hacking.rst000066400000000000000000000105721305063162100172620ustar00rootroot00000000000000================ Hacking on Qtile ================ Requirements ============ Any reasonably recent version of these should work, so you can probably just install them from your package manager. * `pytest `_ * `Xephyr `_ * ``xrandr``, ``xterm``, ``xeyes`` and ``xclock`` (``x11-apps`` on ubuntu) On ubuntu, this can be done with ``sudo apt-get install python-pytest xserver-xephyr x11-apps``. Building cffi module ==================== Qtile ships with a small in-tree pangocairo binding built using cffi, ``pangocffi.py``, and also binds to xcursor with cffi. The bindings are not built at run time and will have to be generated manually when the code is downloaded or when any changes are made to the cffi library. This can be done by calling: .. code-block:: bash python libqtile/ffi_build.py Using Xephyr and the test suite =============================== Qtile has a very extensive test suite, using the Xephyr nested X server. When tests are run, a nested X server with a nested instance of Qtile is fired up, and then tests interact with the Qtile instance through the client API. The fact that we can do this is a great demonstration of just how completely scriptable Qtile is. In fact, Qtile is designed expressly to be scriptable enough to allow unit testing in a nested environment. The Qtile repo includes a tiny helper script to let you quickly pull up a nested instance of Qtile in Xephyr, using your current configuration. Run it from the top-level of the repository, like this: .. code-block:: bash ./scripts/xephyr In practice, the development cycle looks something like this: 1. make minor code change #. run appropriate test: ``pytest tests/test_module.py`` #. GOTO 1, until hackage is complete #. run entire test suite: ``pytest`` #. commit Second X Session ================ Some users prefer to test Qtile in a second, completely separate X session: Just switch to a new tty and run ``startx`` normally to use the ``~/.xinitrc`` X startup script. It's likely though that you want to use a different, customized startup script for testing purposes, for example ``~/.config/qtile/xinitrc``. You can do so by launching X with: .. code-block:: bash startx ~/.config/qtile/xinitrc ``startx`` deals with multiple X sessions automatically. If you want to use ``xinit`` instead, you need to first copy ``/etc/X11/xinit/xserverrc`` to ``~/.xserverrc``; when launching it, you have to specify a new session number: .. code-block:: bash xinit ~/.config/qtile/xinitrc -- :1 Examples of custom X startup scripts are available in `qtile-examples `_. Capturing an ``xtrace`` ======================= Occasionally, a bug will be low level enough to require an ``xtrace`` of Qtile's conversations with the X server. To capture one of these, create an ``xinitrc`` or similar file with: .. code-block:: bash exec xtrace qtile >> ~/qtile.log This will put the xtrace output in Qtile's logfile as well. You can then demonstrate the bug, and paste the contents of this file into the bug report. Coding style ============ While not all of our code follows `PEP8 `_, we do try to adhere to it where possible. All new code should be PEP8 compliant. The ``make lint`` command will run a linter with our configuration over libqtile to ensure your patch complies with reasonable formatting constraints. We also request that git commit messages follow the `standard format `_. Deprecation policy ================== When a widget API is changed, you should deprecate the change using ``libqtile.widget.base.deprecated`` to warn users, in addition to adding it to the appropriate place in the changelog. We will typically remove deprecated APIs one tag after they are deprecated. Testing ======= Of course, your patches should also pass the unit tests as well (i.e. ``make check``). These will be run by travis-ci on every pull request so you can see whether or not your contribution passes. Resources ========= Here are a number of resources that may come in handy: * `Inter-Client Conventions Manual `_ * `Extended Window Manager Hints `_ * `A reasonable basic Xlib Manual `_ qtile-0.10.7/docs/manual/install/000077500000000000000000000000001305063162100165655ustar00rootroot00000000000000qtile-0.10.7/docs/manual/install/arch.rst000066400000000000000000000032311305063162100202330ustar00rootroot00000000000000======================== Installing on Arch Linux ======================== Qtile is available on the `AUR`_ as: ======================= ======================= Package Name Description ======================= ======================= `qtile`_ stable branch (release) `qtile-python3-git`_ development branch ======================= ======================= Using an AUR Helper =================== The preferred way to install Qtile is with an `AUR helper`_. For example, if you use `yaourt`_: .. code-block:: bash yaourt -S Using makepkg ============= The latest version of either package can be obtained by downloading a snapshot or cloning its repository: .. code-block:: bash # snapshot curl -s https://aur.archlinux.org/cgit/aur.git/snapshot/.tar.gz | tar -xvzf - # or repository git clone https://aur.archlinux.org/.git Next makepkg has to be called in the directory where the files were saved. It installs missing dependencies using pacman, builds the package, installs it and removes obsolete build-time dependencies afterwards: .. code-block:: bash cd makepkg -sri Please see the ArchWiki for more information on `installing packages from the AUR`_. .. _AUR: https://wiki.archlinux.org/index.php/AUR .. _AUR Helper: https://wiki.archlinux.org/index.php/AUR_Helpers .. _installing packages from the AUR: https://wiki.archlinux.org/index.php/Arch_User_Repository#Installing_packages .. _qtile: https://aur.archlinux.org/packages/qtile/ .. _qtile-python3-git: https://aur.archlinux.org/packages/qtile-python3-git/ .. _yaourt: https://archlinux.fr/yaourt-en qtile-0.10.7/docs/manual/install/fedora.rst000066400000000000000000000003361305063162100205610ustar00rootroot00000000000000==================== Installing on Fedora ==================== Stable versions of Qtile are currently packaged for current versions of Fedora. To install this package, run: .. code-block:: bash dnf -y install qtile qtile-0.10.7/docs/manual/install/funtoo.rst000066400000000000000000000021231305063162100206270ustar00rootroot00000000000000==================== Installing on Funtoo ==================== Latest versions of Qtile are available on Funtoo with Python 2.7, 3.4, and 3.5 implementations. To install it, run: .. code-block:: bash emerge -av x11-wm/qtile You can also install the development version from GitHub: .. code-block:: bash echo "x11-wm/qtile-9999 **" >> /etc/portage/package.accept_keywords emerge -av qtile Customize ========= You can customize your installation with the following useflags: - dbus - widget-khal-calendar - widget-imap - widget-keyboardkbdd - widget-launchbar - widget-mpd - widget-mpris - widget-wlan The dbus useflag is enabled by default. Disable it only if you know what it is and know you don't use/need it. All widget-* useflags are disabled by default because these widgets require additional dependencies while not everyone will use them. Enable only widgets you need to avoid extra dependencies thanks to these useflags. Visit `Funtoo Qtile documentation`_ for more details on Qtile installation on Funtoo. .. _Funtoo Qtile documentation: http://www.funtoo.org/Package:Qtile qtile-0.10.7/docs/manual/install/index.rst000066400000000000000000000076531305063162100204410ustar00rootroot00000000000000================ Installing Qtile ================ Distro Guides ============= Below are the preferred installation methods for specific distros. If you are running something else, please see `Installing From Source`_. .. toctree:: :maxdepth: 1 Arch Fedora Funtoo Ubuntu/Debian Slackware Installing From Source ====================== First, you need to install all of Qtile's dependencies (although some are optional/not needed depending on your Python version, as noted below). Note that Python 3 versions 3.3 and newer are currently supported and tested. xcffib ------ Qtile uses xcffib_ as an XCB binding, which has its own instructions for building from source. However, if you'd like to skip building it, you can install its dependencies, you will need libxcb and libffi with the associated headers (``libxcb-render0-dev`` and ``libffi-dev`` on Ubuntu), and install it via PyPI: .. code-block:: bash pip install xcffib .. _xcffib: https://github.com/tych0/xcffib#installation cairocffi --------- Qtile uses cairocffi_ with XCB support via xcffib. You'll need ``libcairo2``, the underlying library used by the binding. You should be sure before you install cairocffi that xcffib has been installed, otherwise the needed cairo-xcb bindings will not be built. Once you've got the dependencies installed, you can use the latest version on PyPI: .. code-block:: bash pip install cairocffi .. _cairocffi: https://pythonhosted.org/cairocffi/overview.html pangocairo ---------- You'll also need ``libpangocairo``, which on Ubuntu can be installed via ``sudo apt-get install libpangocairo-1.0-0``. Qtile uses this to provide text rendering (and binds directly to it via cffi with a small in-tree binding). asyncio/trollius ---------------- Qtile uses the asyncio module as introduced in `PEP 3156`_ for its event loop. Based on your Python version, there are different ways to install this: - Python >=3.4: The `asyncio module`_ comes as part of the standard library, so there is nothing more to install. - Python 3.3: This has all the infrastructure needed to implement PEP 3156, but the asyncio module must be installed from the `Tulip project`_. This is done by calling: .. code-block:: bash pip install asyncio Alternatively, you can install trollius (see next point). Note, however, that `trollius is deprecated`_, and it is recommended that you use tulip, as trollius will likely be dropped if (and when) Python 2 support is dropped. - Python 2 (and 3.3 without asyncio): You will need to install trollius_, which backports the asyncio module functionality to work without the infrastructure introduced in PEP 3156. You can install this from PyPI: .. code-block:: bash pip install trollius .. _PEP 3156: http://python.org/dev/peps/pep-3156/ .. _asyncio module: https://docs.python.org/3/library/asyncio.html .. _Tulip project: https://code.google.com/p/tulip/ .. _trollius: http://trollius.readthedocs.io/ .. _trollius is deprecated: http://trollius.readthedocs.io/deprecated.html#deprecated dbus/gobject ------------ Until someone comes along and writes an asyncio-based dbus library, qtile will depend on ``python-dbus`` to interact with dbus. This means that if you want to use things like notification daemon or mpris widgets, you'll need to install python-gobject and python-dbus. Qtile will run fine without these, although it will emit a warning that some things won't work. Qtile ----- With the dependencies in place, you can now install qtile: .. code-block:: bash git clone git://github.com/qtile/qtile.git cd qtile sudo python setup.py install Stable versions of Qtile can be installed from PyPI: .. code-block:: bash pip install qtile As long as the necessary libraries are in place, this can be done at any point, however, it is recommended that you first install xcffib to ensure the cairo-xcb bindings are built (see above). qtile-0.10.7/docs/manual/install/slackware.rst000066400000000000000000000016751305063162100213040ustar00rootroot00000000000000======================= Installing on Slackware ======================= Qtile is available on the `SlackBuilds.org `_ as: ======================= ======================= Package Name Description ======================= ======================= qtile stable branch (release) ======================= ======================= Using slpkg (third party package manager) ========================================= The easy way to install Qtile is with `slpkg `_. For example: .. code-block:: bash slpkg -s sbo qtile Manual installation =================== Download dependencies first and install them. The order in which you need to install is: - pycparser - cffi - six - futures - python-xcffib - trollius - cairocffi - qtile Please see the HOWTO for more information on `SlackBuild Usage HOWTO `_. qtile-0.10.7/docs/manual/install/ubuntu.rst000066400000000000000000000006641305063162100206470ustar00rootroot00000000000000============================== Installing on Debian or Ubuntu ============================== On recent Ubuntu (Yakkety+) and Debian unstable (Sid) versions, there are Qtile packages available via: .. code-block:: bash sudo apt-get install qtile On older versions of Ubuntu (Wily+) and Debian testing (Stretch), the dependencies are available via: .. code-block:: bash sudo apt-get install python3-xcffib python3-cairocffi qtile-0.10.7/docs/manual/license.rst000066400000000000000000000022031305063162100172700ustar00rootroot00000000000000======= License ======= This project is distributed under the MIT license. Copyright (c) 2008, Aldo Cortesi All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. qtile-0.10.7/docs/manual/ref/000077500000000000000000000000001305063162100156735ustar00rootroot00000000000000qtile-0.10.7/docs/manual/ref/commands.rst000066400000000000000000000011761305063162100202330ustar00rootroot00000000000000================== Scripting Commands ================== Here is documented some of the commands available on objects in the command tree when running qshell or scripting commands to qtile. Note that this is an incomplete list, some objects, such as :ref:`layouts ` and :ref:`widgets `, may implement their own set of commands beyond those given here. .. _qtile_commands: .. qtile_class:: libqtile.manager.Qtile .. qtile_class:: libqtile.bar.Bar :noindex: .. qtile_class:: libqtile.config.Group :noindex: .. qtile_class:: libqtile.config.Screen :noindex: .. qtile_class:: libqtile.window.Window qtile-0.10.7/docs/manual/ref/hooks.rst000066400000000000000000000001271305063162100175500ustar00rootroot00000000000000============== Built-in Hooks ============== .. qtile_hooks:: libqtile.hook.subscribe qtile-0.10.7/docs/manual/ref/index.rst000066400000000000000000000001551305063162100175350ustar00rootroot00000000000000========= Reference ========= .. toctree:: :maxdepth: 1 commands hooks layouts widgets qtile-0.10.7/docs/manual/ref/layouts.rst000066400000000000000000000016451305063162100201330ustar00rootroot00000000000000.. _ref_layouts: ================ Built-in Layouts ================ .. qtile_class:: libqtile.layout.floating.Floating :no-commands: .. qtile_class:: libqtile.layout.columns.Columns :no-commands: .. qtile_class:: libqtile.layout.matrix.Matrix :no-commands: .. qtile_class:: libqtile.layout.max.Max :no-commands: .. qtile_class:: libqtile.layout.xmonad.MonadTall :no-commands: .. qtile_class:: libqtile.layout.ratiotile.RatioTile :no-commands: .. qtile_class:: libqtile.layout.slice.Slice :no-commands: .. qtile_class:: libqtile.layout.stack.Stack :no-commands: .. qtile_class:: libqtile.layout.tile.Tile :no-commands: .. qtile_class:: libqtile.layout.tree.TreeTab :no-commands: .. qtile_class:: libqtile.layout.verticaltile.VerticalTile :no-commands: .. qtile_class:: libqtile.layout.wmii.Wmii :no-commands: .. qtile_class:: libqtile.layout.zoomy.Zoomy :no-commands: qtile-0.10.7/docs/manual/ref/widgets.rst000066400000000000000000000002471305063162100200760ustar00rootroot00000000000000.. _ref_widgets: ================ Built-in Widgets ================ .. qtile_module:: libqtile.widget :baseclass: libqtile.widget.base._Widget :no-commands: qtile-0.10.7/docs/requirements.txt000066400000000000000000000001201305063162100171170ustar00rootroot00000000000000mock # for python <= 3.2 sphinx sphinx_rtd_theme sphinxcontrib-seqdiag numpydoc qtile-0.10.7/docs/sphinx_qtile.py000066400000000000000000000150611305063162100167260ustar00rootroot00000000000000# Copyright (c) 2015 dmpayton # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import importlib from docutils import nodes from docutils.statemachine import ViewList from jinja2 import Template from libqtile import command, configurable, widget from six import class_types from six.moves import builtins, reduce from sphinx.util.compat import Directive from sphinx.util.nodes import nested_parse_with_titles qtile_module_template = Template(''' .. qtile_class:: {{ module }}.{{ class_name }} {% if no_config %}:no-config:{% endif %} {% if no_commands %}:no-commands:{% endif %} ''') qtile_class_template = Template(''' {{ class_name }} {{ class_underline }} .. autoclass:: {{ module }}.{{ class_name }}{% for arg in extra_arguments %} {{ arg }}{% endfor %} {% if is_widget %} Supported bar orientations: {{ obj.orientations }} {% endif %} {% if configurable %} .. list-table:: :widths: 20 20 60 :header-rows: 1 * - key - default - description {% for key, default, description in defaults %} * - ``{{ key }}`` - ``{{ default|pprint }}`` - {{ description }} {% endfor %} {% endif %} {% if commandable %} {% for cmd in commands %} .. automethod:: {{ module }}.{{ class_name }}.{{ cmd }} {% endfor %} {% endif %} ''') qtile_hooks_template = Template(''' .. automethod:: libqtile.hook.subscribe.{{ method }} ''') # Adapted from sphinxcontrib-httpdomain def import_object(module_name, expr): mod = __import__(module_name) mod = reduce(getattr, module_name.split('.')[1:], mod) globals = builtins if not isinstance(globals, dict): globals = globals.__dict__ return eval(expr, globals, mod.__dict__) class SimpleDirectiveMixin(object): has_content = True required_arguments = 1 def make_rst(self): raise NotImplementedError def run(self): node = nodes.section() node.document = self.state.document result = ViewList() for line in self.make_rst(): result.append(line, '<{0}>'.format(self.__class__.__name__)) nested_parse_with_titles(self.state, result, node) return node.children class QtileClass(SimpleDirectiveMixin, Directive): optional_arguments = 2 def make_rst(self): module, class_name = self.arguments[0].rsplit('.', 1) arguments = self.arguments[1:] obj = import_object(module, class_name) is_configurable = ':no-config:' not in arguments is_commandable = ':no-commands:' not in arguments arguments = [i for i in arguments if i not in (':no-config:', ':no-commands:')] # build up a dict of defaults using reverse MRO defaults = {} for klass in reversed(obj.mro()): if not issubclass(klass, configurable.Configurable): continue if not hasattr(klass, "defaults"): continue klass_defaults = getattr(klass, "defaults") defaults.update({ d[0]: d[1:] for d in klass_defaults }) # turn the dict into a list of ("value", "default", "description") tuples defaults = [ (k, v[0], v[1]) for k, v in sorted(defaults.items()) ] context = { 'module': module, 'class_name': class_name, 'class_underline': "=" * len(class_name), 'obj': obj, 'defaults': defaults, 'configurable': is_configurable and issubclass(obj, configurable.Configurable), 'commandable': is_commandable and issubclass(obj, command.CommandObject), 'is_widget': issubclass(obj, widget.base._Widget), 'extra_arguments': arguments, } if context['commandable']: context['commands'] = [attr for attr in dir(obj) if attr.startswith('cmd_')] rst = qtile_class_template.render(**context) for line in rst.splitlines(): yield line class QtileHooks(SimpleDirectiveMixin, Directive): def make_rst(self): module, class_name = self.arguments[0].rsplit('.', 1) obj = import_object(module, class_name) for method in sorted(obj.hooks): rst = qtile_hooks_template.render(method=method) for line in rst.splitlines(): yield line class QtileModule(SimpleDirectiveMixin, Directive): # :baseclass: # :no-commands: # :no-config: optional_arguments = 4 def make_rst(self): module = importlib.import_module(self.arguments[0]) BaseClass = None if ':baseclass:' in self.arguments: BaseClass = import_object(*self.arguments[ self.arguments.index(':baseclass:') + 1].rsplit('.', 1)) for item in dir(module): obj = import_object(self.arguments[0], item) if not isinstance(obj, class_types) and (BaseClass and not isinstance(obj, BaseClass)): continue context = { 'module': self.arguments[0], 'class_name': item, 'no_config': ':no-config:' in self.arguments, 'no_commands': ':no-commands:' in self.arguments, } rst = qtile_module_template.render(**context) for line in rst.splitlines(): if not line.strip(): continue yield line def setup(app): app.add_directive('qtile_class', QtileClass) app.add_directive('qtile_hooks', QtileHooks) app.add_directive('qtile_module', QtileModule) qtile-0.10.7/libqtile/000077500000000000000000000000001305063162100145175ustar00rootroot00000000000000qtile-0.10.7/libqtile/__init__.py000066400000000000000000000033231305063162100166310ustar00rootroot00000000000000# Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Tycho Andersen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import absolute_import import six moves = [ six.MovedAttribute("getoutput", "commands", "subprocess"), ] for m in moves: six.add_move(m) # Here, we can't use six.Moved* methods because being able to import asyncio vs # trollius is not strictly Py 2 vs Py 3, but rather asyncio for >=3.4, and # possibly 3.3 with Tulip, and trollius for 2 and <=3.2, and 3.3 without Tulip. # We can no longer direct assign to six.moves, so let's just leave it here so # we don't need to keep try/except importing asyncio. try: import asyncio # noqa except ImportError: import trollius as asyncio # noqa qtile-0.10.7/libqtile/bar.py000066400000000000000000000247161305063162100156470ustar00rootroot00000000000000# Copyright (c) 2008, Aldo Cortesi. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import division from . import command from . import confreader from . import drawer from . import configurable from . import window class Gap(command.CommandObject): """A gap placed along one of the edges of the screen If a gap has been defined, Qtile will avoid covering it with windows. The most probable reason for configuring a gap is to make space for a third-party bar or other static window. Parameters ========== size : The "thickness" of the gap, i.e. the height of a horizontal gap, or the width of a vertical gap. """ def __init__(self, size): """ """ # 'size' corresponds to the height of a horizontal gap, or the width # of a vertical gap self.size = size self.initial_size = size # 'length' corresponds to the width of a horizontal gap, or the height # of a vertical gap self.length = None self.qtile = None self.screen = None self.x = None self.y = None self.width = None self.height = None self.horizontal = None def _configure(self, qtile, screen): self.qtile = qtile self.screen = screen # If both horizontal and vertical gaps are present, screen corners are # given to the horizontal ones if screen.top is self: self.x = screen.x self.y = screen.y self.length = screen.width self.width = self.length self.height = self.size self.horizontal = True elif screen.bottom is self: self.x = screen.x self.y = screen.dy + screen.dheight self.length = screen.width self.width = self.length self.height = self.size self.horizontal = True elif screen.left is self: self.x = screen.x self.y = screen.dy self.length = screen.dheight self.width = self.size self.height = self.length self.horizontal = False else: # right self.x = screen.dx + screen.dwidth self.y = screen.dy self.length = screen.dheight self.width = self.size self.height = self.length self.horizontal = False def draw(self): pass def finalize(self): pass def geometry(self): return (self.x, self.y, self.width, self.height) def _items(self, name): if name == "screen": return (True, None) def _select(self, name, sel): if name == "screen": return self.screen @property def position(self): for i in ["top", "bottom", "left", "right"]: if getattr(self.screen, i) is self: return i def info(self): return dict(position=self.position) def cmd_info(self): """ Info for this object. """ return self.info() class Obj(object): def __init__(self, name): self.name = name def __str__(self): return self.name def __repr__(self): return self.name STRETCH = Obj("STRETCH") CALCULATED = Obj("CALCULATED") STATIC = Obj("STATIC") class Bar(Gap, configurable.Configurable): """A bar, which can contain widgets Parameters ========== widgets : A list of widget objects. size : The "thickness" of the bar, i.e. the height of a horizontal bar, or the width of a vertical bar. """ defaults = [ ("background", "#000000", "Background colour."), ("opacity", 1, "Bar window opacity."), ] def __init__(self, widgets, size, **config): Gap.__init__(self, size) configurable.Configurable.__init__(self, **config) self.add_defaults(Bar.defaults) self.widgets = widgets self.saved_focus = None self.queued_draws = 0 def _configure(self, qtile, screen): Gap._configure(self, qtile, screen) stretches = 0 for w in self.widgets: # Executing _test_orientation_compatibility later, for example in # the _configure() method of each widget, would still pass # test/test_bar.py but a segfault would be raised when nosetests is # about to exit w._test_orientation_compatibility(self.horizontal) if w.length_type == STRETCH: stretches += 1 if stretches > 1: raise confreader.ConfigError("Only one STRETCH widget allowed!") self.window = window.Internal.create( self.qtile, self.x, self.y, self.width, self.height, self.opacity ) self.drawer = drawer.Drawer( self.qtile, self.window.window.wid, self.width, self.height ) self.drawer.clear(self.background) self.window.handle_Expose = self.handle_Expose self.window.handle_ButtonPress = self.handle_ButtonPress self.window.handle_ButtonRelease = self.handle_ButtonRelease qtile.windowMap[self.window.window.wid] = self.window self.window.unhide() for i in self.widgets: qtile.registerWidget(i) i._configure(qtile, self) self._resize(self.length, self.widgets) def finalize(self): self.drawer.finalize() def _resize(self, length, widgets): stretches = [i for i in widgets if i.length_type == STRETCH] if stretches: stretchspace = length - sum( [i.length for i in widgets if i.length_type != STRETCH] ) stretchspace = max(stretchspace, 0) astretch = stretchspace // len(stretches) for i in stretches: i.length = astretch if astretch: i.length += stretchspace % astretch offset = 0 if self.horizontal: for i in widgets: i.offsetx = offset i.offsety = 0 offset += i.length else: for i in widgets: i.offsetx = 0 i.offsety = offset offset += i.length def handle_Expose(self, e): self.draw() def get_widget_in_position(self, e): if self.horizontal: for i in self.widgets: if e.event_x < i.offsetx + i.length: return i else: for i in self.widgets: if e.event_y < i.offsety + i.length: return i def handle_ButtonPress(self, e): widget = self.get_widget_in_position(e) if widget: widget.button_press( e.event_x - widget.offsetx, e.event_y - widget.offsety, e.detail ) def handle_ButtonRelease(self, e): widget = self.get_widget_in_position(e) if widget: widget.button_release( e.event_x - widget.offsetx, e.event_y - widget.offsety, e.detail ) def widget_grab_keyboard(self, widget): """ A widget can call this method to grab the keyboard focus and receive keyboard messages. When done, widget_ungrab_keyboard() must be called. """ self.window.handle_KeyPress = widget.handle_KeyPress self.saved_focus = self.qtile.currentWindow self.window.window.set_input_focus() def widget_ungrab_keyboard(self): """ Removes the widget's keyboard handler. """ del self.window.handle_KeyPress if self.saved_focus is not None: self.saved_focus.window.set_input_focus() def draw(self): if self.queued_draws == 0: self.qtile.call_soon(self._actual_draw) self.queued_draws += 1 def _actual_draw(self): self.queued_draws = 0 self._resize(self.length, self.widgets) for i in self.widgets: i.draw() if self.widgets: end = i.offset + i.length if end < self.length: if self.horizontal: self.drawer.draw(offsetx=end, width=self.length - end) else: self.drawer.draw(offsety=end, height=self.length - end) def info(self): return dict( size=self.size, length=self.length, width=self.width, height=self.height, position=self.position, widgets=[i.info() for i in self.widgets], window=self.window.window.wid ) def is_show(self): return self.size != 0 def show(self, is_show=True): if is_show != self.is_show(): if is_show: self.size = self.initial_size self.window.unhide() else: self.size = 0 self.window.hide() def cmd_fake_button_press(self, screen, position, x, y, button=1): """ Fake a mouse-button-press on the bar. Co-ordinates are relative to the top-left corner of the bar. :screen The integer screen offset :position One of "top", "bottom", "left", or "right" """ class _Fake(object): pass fake = _Fake() fake.event_x = x fake.event_y = y fake.detail = button self.handle_ButtonPress(fake) qtile-0.10.7/libqtile/command.py000066400000000000000000000341361305063162100165160ustar00rootroot00000000000000# Copyright (c) 2008, Aldo Cortesi. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import abc import inspect import traceback import os import six import sys from . import ipc from .utils import get_cache_dir from .log_utils import logger class CommandError(Exception): pass class CommandException(Exception): pass class _SelectError(Exception): def __init__(self, name, sel): Exception.__init__(self) self.name = name self.sel = sel SUCCESS = 0 ERROR = 1 EXCEPTION = 2 SOCKBASE = "qtilesocket.%s" def formatSelector(lst): """ Takes a list of (name, sel) tuples, and returns a formatted selector expression. """ expr = [] for name, sel in iter(lst): if expr: expr.append(".") expr.append(name) if sel is not None: expr.append("[%s]" % repr(sel)) return "".join(expr) class _Server(ipc.Server): def __init__(self, fname, qtile, conf, eventloop): if os.path.exists(fname): os.unlink(fname) ipc.Server.__init__(self, fname, self.call, eventloop) self.qtile = qtile self.widgets = {} for i in conf.screens: for j in i.gaps: if hasattr(j, "widgets"): for w in j.widgets: if w.name: self.widgets[w.name] = w def call(self, data): selectors, name, args, kwargs = data try: obj = self.qtile.select(selectors) except _SelectError as v: e = formatSelector([(v.name, v.sel)]) s = formatSelector(selectors) return (ERROR, "No object %s in path '%s'" % (e, s)) cmd = obj.command(name) if not cmd: return (ERROR, "No such command.") logger.info("Command: %s(%s, %s)", name, args, kwargs) try: return (SUCCESS, cmd(*args, **kwargs)) except CommandError as v: return (ERROR, v.args[0]) except Exception as v: return (EXCEPTION, traceback.format_exc()) class _Command(object): def __init__(self, call, selectors, name): """ :command A string command name specification :*args Arguments to be passed to the specified command :*kwargs Arguments to be passed to the specified command """ self.selectors = selectors self.name = name self.call = call def __call__(self, *args, **kwargs): return self.call(self.selectors, self.name, *args, **kwargs) class _CommandTree(six.with_metaclass(abc.ABCMeta)): """A hierarchical collection of objects that contain commands CommandTree objects act as containers, allowing them to be nested. The commands themselves appear on the object as callable attributes. """ def __init__(self, selectors, myselector, parent): self.selectors = selectors self.myselector = myselector self.parent = parent @property def path(self): s = self.selectors[:] if self.name: s += [(self.name, self.myselector)] return formatSelector(s) @property @abc.abstractmethod def name(self): pass @property @abc.abstractmethod def _contains(self): pass def call(self, selectors, name, *args, **kwargs): if self.parent: return self.parent.call(selectors, name, *args, **kwargs) else: raise NotImplementedError() def __getitem__(self, select): if self.myselector: raise KeyError("No such key: %s" % select) return self.__class__(self.selectors, select, self) def __getattr__(self, name): nextSelector = self.selectors[:] if self.name: nextSelector.append((self.name, self.myselector)) if name in self._contains: return _TreeMap[name](nextSelector, None, self) else: return _Command(self.call, nextSelector, name) class _TLayout(_CommandTree): name = "layout" _contains = ["group", "window", "screen"] class _TWidget(_CommandTree): name = "widget" _contains = ["bar", "screen", "group"] class _TBar(_CommandTree): name = "bar" _contains = ["screen"] class _TWindow(_CommandTree): name = "window" _contains = ["group", "screen", "layout"] class _TScreen(_CommandTree): name = "screen" _contains = ["layout", "window", "bar"] class _TGroup(_CommandTree): name = "group" _contains = ["layout", "window", "screen"] _TreeMap = { "layout": _TLayout, "widget": _TWidget, "bar": _TBar, "window": _TWindow, "screen": _TScreen, "group": _TGroup, } class _CommandRoot(six.with_metaclass(abc.ABCMeta, _CommandTree)): """This class constructs the entire hierarchy of callable commands from a conf object""" name = None _contains = ["layout", "widget", "screen", "bar", "window", "group"] def __init__(self): _CommandTree.__init__(self, [], None, None) def __getitem__(self, select): raise KeyError("No such key: %s" % select) @abc.abstractmethod def call(self, selectors, name, *args, **kwargs): """This method is called for issued commands. Parameters ========== selectors : A list of (name, selector) tuples. name : Command name. """ pass def find_sockfile(display=None): """ Finds the appropriate socket file. """ display = display or os.environ.get('DISPLAY') or ':0.0' if '.' not in display: display += '.0' cache_directory = get_cache_dir() return os.path.join(cache_directory, SOCKBASE % display) class Client(_CommandRoot): """Exposes a command tree used to communicate with a running instance of Qtile""" def __init__(self, fname=None, is_json=False): if not fname: fname = find_sockfile() self.client = ipc.Client(fname, is_json) _CommandRoot.__init__(self) def call(self, selectors, name, *args, **kwargs): state, val = self.client.call((selectors, name, args, kwargs)) if state == SUCCESS: return val elif state == ERROR: raise CommandError(val) else: raise CommandException(val) class CommandRoot(_CommandRoot): def __init__(self, qtile): self.qtile = qtile super(CommandRoot, self).__init__() def call(self, selectors, name, *args, **kwargs): state, val = self.qtile.server.call((selectors, name, args, kwargs)) if state == SUCCESS: return val elif state == ERROR: raise CommandError(val) else: raise CommandException(val) class _Call(object): """ Parameters ========== command : A string command name specification args : Arguments to be passed to the specified command kwargs : Arguments to be passed to the specified command """ def __init__(self, selectors, name, *args, **kwargs): self.selectors = selectors self.name = name self.args = args self.kwargs = kwargs # Conditionals self.layout = None def when(self, layout=None, when_floating=True): self.layout = layout self.when_floating = when_floating return self def check(self, q): if self.layout: if self.layout == 'floating': if q.currentWindow.floating: return True return False if q.currentLayout.name != self.layout: return False if q.currentWindow and q.currentWindow.floating \ and not self.when_floating: return False return True class _LazyTree(_CommandRoot): def call(self, selectors, name, *args, **kwargs): return _Call(selectors, name, *args, **kwargs) lazy = _LazyTree() class CommandObject(six.with_metaclass(abc.ABCMeta)): """Base class for objects that expose commands Each command should be a method named `cmd_X`, where X is the command name. A CommandObject should also implement `._items()` and `._select()` methods (c.f. docstring for `.items()` and `.select()`). """ def select(self, selectors): """Return a selected object Recursively finds an object specified by a list of `(name, selector)` items. Raises _SelectError if the object does not exist. """ if not selectors: return self name, selector = selectors[0] next_selector = selectors[1:] root, items = self.items(name) # if non-root object and no selector given # if no items in container, but selector is given # if selector is not in the list of contained items if (root is False and selector is None) or \ (items is None and selector is not None) or \ (items is not None and selector and selector not in items): raise _SelectError(name, selector) obj = self._select(name, selector) if obj is None: raise _SelectError(name, selector) return obj.select(next_selector) def items(self, name): """Build a list of contained items for the given item class Returns a tuple `(root, items)` for the specified item class, where: root: True if this class accepts a "naked" specification without an item seletion (e.g. "layout" defaults to current layout), and False if it does not (e.g. no default "widget"). items: a list of contained items """ ret = self._items(name) if ret is None: # Not finding information for a particular item class is OK here; # we don't expect layouts to have a window, etc. return False, [] return ret @abc.abstractmethod def _items(self, name): """Generate the items for a given Same return as `.items()`. Return `None` if name is not a valid item class. """ pass @abc.abstractmethod def _select(self, name, sel): """Select the given item of the given item class This method is called with the following guarantees: - `name` is a valid selector class for this item - `sel` is a valid selector for this item - the `(name, sel)` tuple is not an "impossible" combination (e.g. a selector is specified when `name` is not a containment object). Return None if no such object exists """ pass def command(self, name): return getattr(self, "cmd_" + name, None) @property def commands(self): cmds = [i[4:] for i in dir(self) if i.startswith("cmd_")] return cmds def cmd_commands(self): """Returns a list of possible commands for this object Used by __qsh__ for command completion and online help """ return self.commands def cmd_items(self, name): """Returns a list of contained items for the specified name Used by __qsh__ to allow navigation of the object graph. """ return self.items(name) def docSig(self, name): # inspect.signature introduced in Python 3.3 if sys.version_info < (3, 3): args, varargs, varkw, defaults = inspect.getargspec(self.command(name)) if args and args[0] == "self": args = args[1:] return name + inspect.formatargspec(args, varargs, varkw, defaults) sig = inspect.signature(self.command(name)) args = list(sig.parameters) if args and args[0] == "self": args = args[1:] sig = sig.replace(parameters=args) return name + str(sig) def docText(self, name): return inspect.getdoc(self.command(name)) or "" def doc(self, name): spec = self.docSig(name) htext = self.docText(name) return spec + '\n' + htext def cmd_doc(self, name): """Returns the documentation for a specified command name Used by __qsh__ to provide online help. """ if name in self.commands: return self.doc(name) else: raise CommandError("No such command: %s" % name) def cmd_eval(self, code): """Evaluates code in the same context as this function Return value is tuple `(success, result)`, success being a boolean and result being a string representing the return value of eval, or None if exec was used instead. """ try: try: return (True, str(eval(code))) except SyntaxError: exec(code) return (True, None) except: error = traceback.format_exc().strip().split("\n")[-1] return (False, error) def cmd_function(self, function, *args, **kwargs): """Call a function with current object as argument""" try: function(self, *args, **kwargs) except Exception: error = traceback.format_exc() logger.error('Exception calling "%s":\n%s' % (function, error)) qtile-0.10.7/libqtile/config.py000066400000000000000000000461551305063162100163510ustar00rootroot00000000000000# Copyright (c) 2012-2015 Tycho Andersen # Copyright (c) 2013 xarvh # Copyright (c) 2013 horsik # Copyright (c) 2013-2014 roger # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Adi Sieker # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from . import command from . import hook from . import utils from . import xcbq from six import MAXSIZE import warnings class Key(object): """Defines a keybinding. Parameters ========== modifiers: A list of modifier specifications. Modifier specifications are one of: "shift", "lock", "control", "mod1", "mod2", "mod3", "mod4", "mod5". key: A key specification, e.g. "a", "Tab", "Return", "space". commands: A list of lazy command objects generated with the command.lazy helper. If multiple Call objects are specified, they are run in sequence. kwds: A dictionary containing "desc", allowing a description to be added """ def __init__(self, modifiers, key, *commands, **kwds): self.modifiers = modifiers self.key = key self.commands = commands self.desc = kwds.get("desc", "") if key not in xcbq.keysyms: raise utils.QtileError("Unknown key: %s" % key) self.keysym = xcbq.keysyms[key] try: self.modmask = utils.translate_masks(self.modifiers) except KeyError as v: raise utils.QtileError(v) def __repr__(self): return "" % (self.modifiers, self.key) class Drag(object): """Defines binding of a mouse to some dragging action On each motion event command is executed with two extra parameters added x and y offset from previous move It focuses clicked window by default. If you want to prevent it pass, `focus=None` as an argument """ def __init__(self, modifiers, button, *commands, **kwargs): self.start = kwargs.get("start") self.focus = kwargs.get("focus", "before") self.modifiers = modifiers self.button = button self.commands = commands try: self.button_code = int(self.button.replace('Button', '')) self.modmask = utils.translate_masks(self.modifiers) except KeyError as v: raise utils.QtileError(v) def __repr__(self): return "" % (self.modifiers, self.button) class Click(object): """Defines binding of a mouse click It focuses clicked window by default. If you want to prevent it, pass `focus=None` as an argument """ def __init__(self, modifiers, button, *commands, **kwargs): self.focus = kwargs.get("focus", "before") self.modifiers = modifiers self.button = button self.commands = commands try: self.button_code = int(self.button.replace('Button', '')) self.modmask = utils.translate_masks(self.modifiers) except KeyError as v: raise utils.QtileError(v) def __repr__(self): return "" % (self.modifiers, self.button) class EzConfig(object): """ Helper class for defining key and button bindings in an emacs-like format. Inspired by Xmonad's XMonad.Util.EZConfig. """ modifier_keys = { 'M': 'mod4', 'A': 'mod1', 'S': 'shift', 'C': 'control', } def parse(self, spec): """ Splits an emacs keydef into modifiers and keys. For example: "M-S-a" -> ['mod4', 'shift'], 'a' "A-" -> ['mod1'], 'minus' "C-" -> ['control'], 'Tab' """ mods = [] keys = [] for key in spec.split('-'): if not key: break if key in self.modifier_keys: if keys: msg = 'Modifiers must always come before key/btn: %s' raise utils.QtileError(msg % spec) mods.append(self.modifier_keys[key]) continue if len(key) == 1: keys.append(key) continue if len(key) > 3 and key[0] == '<' and key[-1] == '>': keys.append(key[1:-1]) continue if not keys: msg = 'Invalid key/btn specifier: %s' raise utils.QtileError(msg % spec) if len(keys) > 1: msg = 'Key chains are not supported: %s' % spec raise utils.QtileError(msg) return mods, keys[0] class EzKey(EzConfig, Key): def __init__(self, keydef, *commands): modkeys, key = self.parse(keydef) super(EzKey, self).__init__(modkeys, key, *commands) class EzClick(EzConfig, Click): def __init__(self, btndef, *commands, **kwargs): modkeys, button = self.parse(btndef) button = 'Button%s' % button super(EzClick, self).__init__(modkeys, button, *commands, **kwargs) class EzDrag(EzConfig, Drag): def __init__(self, btndef, *commands, **kwargs): modkeys, button = self.parse(btndef) button = 'Button%s' % button super(EzDrag, self).__init__(modkeys, button, *commands, **kwargs) class ScreenRect(object): def __init__(self, x, y, width, height): self.x = x self.y = y self.width = width self.height = height def __repr__(self): return '<%s %d,%d %d,%d>' % ( self.__class__.__name__, self.x, self.y, self.width, self.height ) def hsplit(self, columnwidth): assert columnwidth > 0 assert columnwidth < self.width return ( self.__class__(self.x, self.y, columnwidth, self.height), self.__class__( self.x + columnwidth, self.y, self.width - columnwidth, self.height ) ) def vsplit(self, rowheight): assert rowheight > 0 assert rowheight < self.height return ( self.__class__(self.x, self.y, self.width, rowheight), self.__class__( self.x, self.y + rowheight, self.width, self.height - rowheight ) ) class Screen(command.CommandObject): """A physical screen, and its associated paraphernalia. Define a screen with a given set of Bars of a specific geometry. Note that bar.Bar objects can only be placed at the top or the bottom of the screen (bar.Gap objects can be placed anywhere). Also, ``x``, ``y``, ``width``, and ``height`` aren't specified usually unless you are using 'fake screens'. Parameters ========== top: List of Gap/Bar objects, or None. bottom: List of Gap/Bar objects, or None. left: List of Gap/Bar objects, or None. right: List of Gap/Bar objects, or None. x : int or None y : int or None width : int or None height : int or None """ def __init__(self, top=None, bottom=None, left=None, right=None, x=None, y=None, width=None, height=None): self.group = None self.previous_group = None self.top = top self.bottom = bottom self.left = left self.right = right self.qtile = None self.index = None # x position of upper left corner can be > 0 # if one screen is "right" of the other self.x = x self.y = y self.width = width self.height = height def _configure(self, qtile, index, x, y, width, height, group): self.qtile = qtile self.index = index self.x = x self.y = y self.width = width self.height = height self.setGroup(group) for i in self.gaps: i._configure(qtile, self) @property def gaps(self): return (i for i in [self.top, self.bottom, self.left, self.right] if i) @property def dx(self): return self.x + self.left.size if self.left else self.x @property def dy(self): return self.y + self.top.size if self.top else self.y @property def dwidth(self): val = self.width if self.left: val -= self.left.size if self.right: val -= self.right.size return val @property def dheight(self): val = self.height if self.top: val -= self.top.size if self.bottom: val -= self.bottom.size return val def get_rect(self): return ScreenRect(self.dx, self.dy, self.dwidth, self.dheight) def setGroup(self, new_group, save_prev=True): """Put group on this screen""" if new_group.screen == self: return if save_prev: self.previous_group = self.group if new_group is None: return if new_group.screen: # g1 <-> s1 (self) # g2 (new_group) <-> s2 to # g1 <-> s2 # g2 <-> s1 g1 = self.group s1 = self g2 = new_group s2 = new_group.screen s2.group = g1 g1._setScreen(s2) s1.group = g2 g2._setScreen(s1) else: old_group = self.group self.group = new_group # display clients of the new group and then hide from old group # to remove the screen flickering new_group._setScreen(self) if old_group is not None: old_group._setScreen(None) hook.fire("setgroup") hook.fire("focus_change") hook.fire( "layout_change", self.group.layouts[self.group.currentLayout], self.group ) def _items(self, name): if name == "layout": return (True, list(range(len(self.group.layouts)))) elif name == "window": return (True, [i.window.wid for i in self.group.windows]) elif name == "bar": return (False, [x.position for x in self.gaps]) def _select(self, name, sel): if name == "layout": if sel is None: return self.group.layout else: return utils.lget(self.group.layouts, sel) elif name == "window": if sel is None: return self.group.currentWindow else: for i in self.group.windows: if i.window.wid == sel: return i elif name == "bar": return getattr(self, sel) def resize(self, x=None, y=None, w=None, h=None): x = x or self.x y = y or self.y w = w or self.width h = h or self.height self._configure(self.qtile, self.index, x, y, w, h, self.group) for bar in [self.top, self.bottom, self.left, self.right]: if bar: bar.draw() self.qtile.call_soon(self.group.layoutAll) def cmd_info(self): """ Returns a dictionary of info for this screen. """ return dict( index=self.index, width=self.width, height=self.height, x=self.x, y=self.y ) def cmd_resize(self, x=None, y=None, w=None, h=None): """Resize the screen""" self.resize(x, y, w, h) def cmd_next_group(self, skip_empty=False, skip_managed=False): """Switch to the next group""" n = self.group.nextGroup(skip_empty, skip_managed) self.setGroup(n) return n.name def cmd_prev_group(self, skip_empty=False, skip_managed=False): """Switch to the previous group""" n = self.group.prevGroup(skip_empty, skip_managed) self.setGroup(n) return n.name def cmd_toggle_group(self, group_name=None): """Switch to the selected group or to the previously active one""" group = self.qtile.groupMap.get(group_name) if group in (self.group, None): group = self.previous_group self.setGroup(group) def cmd_togglegroup(self, groupName=None): """Switch to the selected group or to the previously active one Deprecated: use toggle_group()""" warnings.warn("togglegroup is deprecated, use toggle_group", DeprecationWarning) self.cmd_toggle_group(groupName) class Group(object): """Represents a "dynamic" group These groups can spawn apps, only allow certain Matched windows to be on them, hide when they're not in use, etc. Parameters ========== name : string the name of this group matches : default ``None`` list of ``Match`` objects whose windows will be assigned to this group exclusive : boolean when other apps are started in this group, should we allow them here or not? spawn : string or list of strings this will be ``exec()`` d when the group is created, you can pass either a program name or a list of programs to ``exec()`` layout : string the default layout for this group (e.g. 'max' or 'stack') layouts : list the group layouts list overriding global layouts persist : boolean should this group stay alive with no member windows? init : boolean is this group alive when qtile starts? position : int group position """ def __init__(self, name, matches=None, exclusive=False, spawn=None, layout=None, layouts=None, persist=True, init=True, layout_opts=None, screen_affinity=None, position=MAXSIZE): self.name = name self.exclusive = exclusive self.spawn = spawn self.layout = layout self.layouts = layouts or [] self.persist = persist self.init = init self.matches = matches or [] self.layout_opts = layout_opts or {} self.screen_affinity = screen_affinity self.position = position def __repr__(self): attrs = utils.describe_attributes(self, ['exclusive', 'spawn', 'layout', 'layouts', 'persist', 'init', 'matches', 'layout_opts', 'screen_affinity']) return '' % (self.name, attrs) class Match(object): """Match for dynamic groups It can match by title, class or role. ``Match`` supports both regular expression objects (i.e. the result of ``re.compile()``) or strings (match as a "include" match). If a window matches any of the things in any of the lists, it is considered a match. Parameters ========== title: things to match against the title (WM_NAME) wm_class: things to match against the second string in WM_CLASS atom role: things to match against the WM_ROLE atom wm_type: things to match against the WM_TYPE atom wm_instance_class: things to match against the first string in WM_CLASS atom net_wm_pid: things to match against the _NET_WM_PID atom (only int allowed in this rule) """ def __init__(self, title=None, wm_class=None, role=None, wm_type=None, wm_instance_class=None, net_wm_pid=None): if not title: title = [] if not wm_class: wm_class = [] if not role: role = [] if not wm_type: wm_type = [] if not wm_instance_class: wm_instance_class = [] if not net_wm_pid: net_wm_pid = [] try: net_wm_pid = list(map(int, net_wm_pid)) except ValueError: error = 'Invalid rule for net_wm_pid: "%s" '\ 'only ints allowed' % str(net_wm_pid) raise utils.QtileError(error) self._rules = [('title', t) for t in title] self._rules += [('wm_class', w) for w in wm_class] self._rules += [('role', r) for r in role] self._rules += [('wm_type', r) for r in wm_type] self._rules += [('wm_instance_class', w) for w in wm_instance_class] self._rules += [('net_wm_pid', w) for w in net_wm_pid] def compare(self, client): for _type, rule in self._rules: if _type == "net_wm_pid": def match_func(value): return rule == value else: match_func = getattr(rule, 'match', None) or \ getattr(rule, 'count') if _type == 'title': value = client.name elif _type == 'wm_class': value = None _value = client.window.get_wm_class() if _value and len(_value) > 1: value = _value[1] elif _type == 'wm_instance_class': value = client.window.get_wm_class() if value: value = value[0] elif _type == 'wm_type': value = client.window.get_wm_type() elif _type == 'net_wm_pid': value = client.window.get_net_wm_pid() else: value = client.window.get_wm_window_role() if value and match_func(value): return True return False def map(self, callback, clients): """Apply callback to each client that matches this Match""" for c in clients: if self.compare(c): callback(c) def __repr__(self): return '' % self._rules class Rule(object): """How to act on a Match A Rule contains a Match object, and a specification about what to do when that object is matched. Parameters ========== match : ``Match`` object associated with this ``Rule`` float : auto float this window? intrusive : override the group's exclusive setting? break_on_match : Should we stop applying rules if this rule is matched? """ def __init__(self, match, group=None, float=False, intrusive=False, break_on_match=True): self.match = match self.group = group self.float = float self.intrusive = intrusive self.break_on_match = break_on_match def matches(self, w): return self.match.compare(w) def __repr__(self): actions = utils.describe_attributes(self, ['group', 'float', 'intrusive', 'break_on_match']) return '' % (self.match, actions) qtile-0.10.7/libqtile/configurable.py000066400000000000000000000055771305063162100175470ustar00rootroot00000000000000# Copyright (c) 2012, Tycho Andersen. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. class Configurable(object): global_defaults = {} def __init__(self, **config): self._widget_defaults = {} self._user_config = config def add_defaults(self, defaults): """Add defaults to this object, overwriting any which already exist""" self._widget_defaults.update(dict((d[0], d[1]) for d in defaults)) def __getattr__(self, name): if name == "_widget_defaults": raise AttributeError found, value = self._find_default(name) if found: setattr(self, name, value) return value else: cname = self.__class__.__name__ raise AttributeError("%s has no attribute: %s" % (cname, name)) def _find_default(self, name): """Returns a tuple (found, value)""" defaults = self._widget_defaults.copy() defaults.update(self.global_defaults) defaults.update(self._user_config) if name in defaults: return (True, defaults[name]) else: return (False, None) class ExtraFallback(object): """Adds another layer of fallback to attributes Used to look up a different attribute name """ def __init__(self, name, fallback): self.name = name self.hidden_attribute = "_" + name self.fallback = fallback def __get__(self, instance, owner=None): retval = getattr(instance, self.hidden_attribute, None) if retval is None: _found, retval = Configurable._find_default(instance, self.name) if retval is None: retval = getattr(instance, self.fallback, None) return retval def __set__(self, instance, value): """Set own value to a hidden attribute of the object""" setattr(instance, self.hidden_attribute, value) qtile-0.10.7/libqtile/confreader.py000066400000000000000000000071421305063162100172050ustar00rootroot00000000000000# coding: utf-8 # # Copyright (c) 2008, Aldo Cortesi # Copyright (c) 2011, Andrew Grigorev # # All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import os import sys import traceback from libqtile.log_utils import logger class ConfigError(Exception): pass class File(object): def __init__(self, fname=None, is_restart=False): if not fname: config_directory = os.path.expandvars('$XDG_CONFIG_HOME') if config_directory == '$XDG_CONFIG_HOME': # if variable wasn't set config_directory = os.path.expanduser("~/.config") fname = os.path.join(config_directory, "qtile", "config.py") # We delay importing here to avoid a circular import issue when # testing. from .resources import default_config if fname == "default": config = default_config elif os.path.isfile(fname): try: sys.path.insert(0, os.path.dirname(fname)) config = __import__(os.path.basename(fname)[:-3]) except Exception as v: tb = traceback.format_exc() # On restart, user potentially has some windows open, but they # screwed up their config. So as not to lose their apps, we # just load the default config here. if is_restart: logger.warning( 'Caught exception in configuration:\n\n' '%s\n\n' 'Qtile restarted with default config', tb) config = None else: raise ConfigError(tb) else: config = None # if you add something here, be sure to add a reasonable default value # to resources/default_config.py config_options = [ "keys", "mouse", "groups", "dgroups_key_binder", "dgroups_app_rules", "follow_mouse_focus", "focus_on_window_activation", "cursor_warp", "layouts", "floating_layout", "screens", "main", "auto_fullscreen", "widget_defaults", "bring_front_click", "wmname", "extentions", ] for option in config_options: if hasattr(config, option): v = getattr(config, option) else: v = getattr(default_config, option) if not hasattr(self, option): setattr(self, option, v) qtile-0.10.7/libqtile/dgroups.py000066400000000000000000000207751305063162100165670ustar00rootroot00000000000000# Copyright (c) 2011-2012 Florian Mounier # Copyright (c) 2012-2014 roger # Copyright (c) 2012 Craig Barnes # Copyright (c) 2012-2014 Tycho Andersen # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sebastian Kricner # Copyright (c) 2014 Sean Vig # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import collections import six import libqtile.hook from libqtile.config import Key from libqtile.command import lazy from libqtile.config import Group from libqtile.config import Rule from libqtile.config import Match from libqtile.log_utils import logger def simple_key_binder(mod, keynames=None): """Bind keys to mod+group position or to the keys specified as second argument""" def func(dgroup): # unbind all for key in dgroup.keys[:]: dgroup.qtile.unmapKey(key) dgroup.keys.remove(key) if keynames: keys = keynames else: # keys 1 to 9 and 0 keys = list(map(str, list(range(1, 10)) + [0])) # bind all keys for keyname, group in zip(keys, dgroup.qtile.groups): name = group.name key = Key([mod], keyname, lazy.group[name].toscreen()) key_s = Key([mod, "shift"], keyname, lazy.window.togroup(name)) key_c = Key( [mod, "control"], keyname, lazy.group.switch_groups(name) ) dgroup.keys.append(key) dgroup.keys.append(key_s) dgroup.keys.append(key_c) dgroup.qtile.mapKey(key) dgroup.qtile.mapKey(key_s) dgroup.qtile.mapKey(key_c) return func class DGroups(object): """Dynamic Groups""" def __init__(self, qtile, dgroups, key_binder=None, delay=1): self.qtile = qtile self.groups = dgroups self.groupMap = {} self.rules = [] self.rules_map = {} self.last_rule_id = 0 for rule in getattr(qtile.config, 'dgroups_app_rules', []): self.add_rule(rule) self.keys = [] self.key_binder = key_binder self._setup_hooks() self._setup_groups() self.delay = delay self.timeout = {} def add_rule(self, rule, last=True): rule_id = self.last_rule_id self.rules_map[rule_id] = rule if last: self.rules.append(rule) else: self.rules.insert(0, rule) self.last_rule_id += 1 return rule_id def remove_rule(self, rule_id): rule = self.rules_map.get(rule_id) if rule: self.rules.remove(rule) del self.rules_map[rule_id] else: logger.warn('Rule "%s" not found', rule_id) def add_dgroup(self, group, start=False): self.groupMap[group.name] = group rules = [Rule(m, group=group.name) for m in group.matches] self.rules.extend(rules) if start: self.qtile.addGroup(group.name, group.layout, group.layouts) def _setup_groups(self): for group in self.groups: self.add_dgroup(group, group.init) if group.spawn and not self.qtile.no_spawn: if isinstance(group.spawn, six.string_types): spawns = [group.spawn] else: spawns = group.spawn for spawn in spawns: pid = self.qtile.cmd_spawn(spawn) self.add_rule(Rule(Match(net_wm_pid=[pid]), group.name)) def _setup_hooks(self): libqtile.hook.subscribe.addgroup(self._addgroup) libqtile.hook.subscribe.client_new(self._add) libqtile.hook.subscribe.client_killed(self._del) if self.key_binder: libqtile.hook.subscribe.setgroup( lambda: self.key_binder(self) ) libqtile.hook.subscribe.changegroup( lambda: self.key_binder(self) ) def _addgroup(self, qtile, group_name): if group_name not in self.groupMap: self.add_dgroup(Group(group_name, persist=False)) def _add(self, client): if client in self.timeout: logger.info('Remove dgroup source') self.timeout.pop(client).cancel() # ignore static windows if client.defunct: return # ignore windows whose groups is already set (e.g. from another hook or # when it was set on state restore) if client.group is not None: return group_set = False intrusive = False for rule in self.rules: # Matching Rules if rule.matches(client): if rule.group: try: layout = self.groupMap[rule.group].layout except KeyError: layout = None try: layouts = self.groupMap[rule.group].layouts except KeyError: layouts = None group_added = self.qtile.addGroup(rule.group, layout, layouts) client.togroup(rule.group) group_set = True group_obj = self.qtile.groupMap[rule.group] group = self.groupMap.get(rule.group) if group and group_added: for k, v in list(group.layout_opts.items()): if isinstance(v, collections.Callable): v(group_obj.layout) else: setattr(group_obj.layout, k, v) affinity = group.screen_affinity if affinity and len(self.qtile.screens) > affinity: self.qtile.screens[affinity].setGroup(group_obj) if rule.float: client.enablefloating() if rule.intrusive: intrusive = rule.intrusive if rule.break_on_match: break # If app doesn't have a group if not group_set: current_group = self.qtile.currentGroup.name if current_group in self.groupMap and \ self.groupMap[current_group].exclusive and \ not intrusive: wm_class = client.window.get_wm_class() if wm_class: if len(wm_class) > 1: wm_class = wm_class[1] else: wm_class = wm_class[0] group_name = wm_class else: group_name = client.name or 'Unnamed' self.add_dgroup(Group(group_name, persist=False), start=True) client.togroup(group_name) self.sort_groups() def sort_groups(self): self.qtile.groups.sort(key=lambda g: self.groupMap[g.name].position) libqtile.hook.fire("setgroup") def _del(self, client): group = client.group def delete_client(): # Delete group if empty and don't persist if group and group.name in self.groupMap and \ not self.groupMap[group.name].persist and \ len(group.windows) <= 0: self.qtile.delGroup(group.name) self.sort_groups() del self.timeout[client] # Wait the delay until really delete the group logger.info('Add dgroup timer') self.timeout[client] = self.qtile.call_later( self.delay, delete_client ) qtile-0.10.7/libqtile/drawer.py000066400000000000000000000343361305063162100163660ustar00rootroot00000000000000# Copyright (c) 2010 Aldo Cortesi # Copyright (c) 2011 Florian Mounier # Copyright (c) 2011 oitel # Copyright (c) 2011 Kenji_Takahashi # Copyright (c) 2011 Paul Colomiets # Copyright (c) 2012, 2014 roger # Copyright (c) 2012 nullzion # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014-2015 Sean Vig # Copyright (c) 2014 Nathan Hoad # Copyright (c) 2014 dequis # Copyright (c) 2014 Tycho Andersen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import division import collections import math import cairocffi import xcffib.xproto from . import pangocffi from . import utils class TextLayout(object): def __init__(self, drawer, text, colour, font_family, font_size, font_shadow, wrap=True, markup=False): self.drawer, self.colour = drawer, colour layout = drawer.ctx.create_layout() layout.set_alignment(pangocffi.ALIGN_CENTER) if not wrap: # pango wraps by default layout.set_ellipsize(pangocffi.ELLIPSIZE_END) desc = pangocffi.FontDescription.from_string(font_family) desc.set_absolute_size(pangocffi.units_from_double(float(font_size))) layout.set_font_description(desc) self.font_shadow = font_shadow self.layout = layout self.markup = markup self.text = text self._width = None def finalize(self): self.layout.finalize() @property def text(self): return self.layout.get_text() @text.setter def text(self, value): if self.markup: # pangocffi doesn't like None here, so we use "". if value is None: value = '' attrlist, value, accel_char = pangocffi.parse_markup(value) self.layout.set_attributes(attrlist) self.layout.set_text(utils.scrub_to_utf8(value)) @property def width(self): if self._width is not None: return self._width else: return self.layout.get_pixel_size()[0] @width.setter def width(self, value): self._width = value self.layout.set_width(pangocffi.units_from_double(value)) @width.deleter def width(self): self._width = None self.layout.set_width(-1) @property def height(self): return self.layout.get_pixel_size()[1] def fontdescription(self): return self.layout.get_font_description() @property def font_family(self): d = self.fontdescription() return d.get_family() @font_family.setter def font_family(self, font): d = self.fontdescription() d.set_family(font) self.layout.set_font_description(d) @property def font_size(self): d = self.fontdescription() return d.get_size() @font_size.setter def font_size(self, size): d = self.fontdescription() d.set_size(size) d.set_absolute_size(pangocffi.units_from_double(size)) self.layout.set_font_description(d) def draw(self, x, y): if self.font_shadow is not None: self.drawer.set_source_rgb(self.font_shadow) self.drawer.ctx.move_to(x + 1, y + 1) self.drawer.ctx.show_layout(self.layout) self.drawer.set_source_rgb(self.colour) self.drawer.ctx.move_to(x, y) self.drawer.ctx.show_layout(self.layout) def framed(self, border_width, border_color, pad_x, pad_y, highlight_color=None): return TextFrame(self, border_width, border_color, pad_x, pad_y, highlight_color=highlight_color) class TextFrame(object): def __init__(self, layout, border_width, border_color, pad_x, pad_y, highlight_color=None): self.layout = layout self.border_width = border_width self.border_color = border_color self.drawer = self.layout.drawer self.highlight_color = highlight_color if isinstance(pad_x, collections.Iterable): self.pad_left = pad_x[0] self.pad_right = pad_x[1] else: self.pad_left = self.pad_right = pad_x if isinstance(pad_y, collections.Iterable): self.pad_top = pad_y[0] self.pad_bottom = pad_y[1] else: self.pad_top = self.pad_bottom = pad_y def draw(self, x, y, rounded=True, fill=False, line=False, highlight=False): self.drawer.set_source_rgb(self.border_color) opts = [ x, y, self.layout.width + self.pad_left + self.pad_right, self.layout.height + self.pad_top + self.pad_bottom, self.border_width ] if line: if highlight: self.drawer.set_source_rgb(self.highlight_color) self.drawer.fillrect(*opts) self.drawer.set_source_rgb(self.border_color) # change to only fill in bottom line opts[1] = self.height - self.border_width # y opts[3] = self.border_width # height self.drawer.fillrect(*opts) elif fill: if rounded: self.drawer.rounded_fillrect(*opts) else: self.drawer.fillrect(*opts) else: if rounded: self.drawer.rounded_rectangle(*opts) else: self.drawer.rectangle(*opts) self.drawer.ctx.stroke() self.layout.draw( x + self.pad_left, y + self.pad_top ) def draw_fill(self, x, y, rounded=True): self.draw(x, y, rounded=rounded, fill=True) def draw_line(self, x, y, highlighted): self.draw(x, y, line=True, highlight=highlighted) @property def height(self): return self.layout.height + self.pad_top + self.pad_bottom @property def width(self): return self.layout.width + self.pad_left + self.pad_right class Drawer(object): """ A helper class for drawing and text layout. We have a drawer object for each widget in the bar. The underlying surface is a pixmap with the same size as the bar itself. We draw to the pixmap starting at offset 0, 0, and when the time comes to display to the window, we copy the appropriate portion of the pixmap onto the window. """ def __init__(self, qtile, wid, width, height): self.qtile = qtile self.wid, self.width, self.height = wid, width, height self.pixmap = self.qtile.conn.conn.generate_id() self.gc = self.qtile.conn.conn.generate_id() self.qtile.conn.conn.core.CreatePixmap( self.qtile.conn.default_screen.root_depth, self.pixmap, self.wid, self.width, self.height ) self.qtile.conn.conn.core.CreateGC( self.gc, self.wid, xcffib.xproto.GC.Foreground | xcffib.xproto.GC.Background, [ self.qtile.conn.default_screen.black_pixel, self.qtile.conn.default_screen.white_pixel ] ) self.surface = cairocffi.XCBSurface( qtile.conn.conn, self.pixmap, self.find_root_visual(), self.width, self.height, ) self.ctx = self.new_ctx() self.clear((0, 0, 1)) def finalize(self): self.qtile.conn.conn.core.FreeGC(self.gc) self.qtile.conn.conn.core.FreePixmap(self.pixmap) self.ctx = None self.surface = None def _rounded_rect(self, x, y, width, height, linewidth): aspect = 1.0 corner_radius = height / 10.0 radius = corner_radius / aspect degrees = math.pi / 180.0 self.ctx.new_sub_path() delta = radius + linewidth / 2 self.ctx.arc(x + width - delta, y + delta, radius, -90 * degrees, 0 * degrees) self.ctx.arc(x + width - delta, y + height - delta, radius, 0 * degrees, 90 * degrees) self.ctx.arc(x + delta, y + height - delta, radius, 90 * degrees, 180 * degrees) self.ctx.arc(x + delta, y + delta, radius, 180 * degrees, 270 * degrees) self.ctx.close_path() def rounded_rectangle(self, x, y, width, height, linewidth): self._rounded_rect(x, y, width, height, linewidth) self.ctx.set_line_width(linewidth) self.ctx.stroke() def rounded_fillrect(self, x, y, width, height, linewidth): self._rounded_rect(x, y, width, height, linewidth) self.ctx.fill() def rectangle(self, x, y, width, height, linewidth=2): self.ctx.set_line_width(linewidth) self.ctx.rectangle(x, y, width, height) self.ctx.stroke() def fillrect(self, x, y, width, height, linewidth=2): self.ctx.set_line_width(linewidth) self.ctx.rectangle(x, y, width, height) self.ctx.fill() self.ctx.stroke() def draw(self, offsetx=0, offsety=0, width=None, height=None): """ Parameters ========== offsetx : the X offset to start drawing at. offsety : the Y offset to start drawing at. width : the X portion of the canvas to draw at the starting point. height : the Y portion of the canvas to draw at the starting point. """ self.qtile.conn.conn.core.CopyArea( self.pixmap, self.wid, self.gc, 0, 0, # srcx, srcy offsetx, offsety, # dstx, dsty self.width if width is None else width, self.height if height is None else height ) def find_root_visual(self): for i in self.qtile.conn.default_screen.allowed_depths: for v in i.visuals: if v.visual_id == self.qtile.conn.default_screen.root_visual: return v def new_ctx(self): return pangocffi.CairoContext(cairocffi.Context(self.surface)) def set_source_rgb(self, colour): if type(colour) == list: if len(colour) == 0: # defaults to black self.ctx.set_source_rgba(*utils.rgb("#000000")) elif len(colour) == 1: self.ctx.set_source_rgba(*utils.rgb(colour[0])) else: linear = cairocffi.LinearGradient(0.0, 0.0, 0.0, self.height) step_size = 1.0 / (len(colour) - 1) step = 0.0 for c in colour: rgb_col = utils.rgb(c) if len(rgb_col) < 4: rgb_col[3] = 1 linear.add_color_stop_rgba(step, *rgb_col) step += step_size self.ctx.set_source(linear) else: self.ctx.set_source_rgba(*utils.rgb(colour)) def clear(self, colour): self.set_source_rgb(colour) self.ctx.rectangle(0, 0, self.width, self.height) self.ctx.fill() self.ctx.stroke() def textlayout(self, text, colour, font_family, font_size, font_shadow, markup=False, **kw): """Get a text layout""" return TextLayout(self, text, colour, font_family, font_size, font_shadow, markup=markup, **kw) def max_layout_size(self, texts, font_family, font_size): sizelayout = self.textlayout( "", "ffffff", font_family, font_size, None) widths, heights = [], [] for i in texts: sizelayout.text = i widths.append(sizelayout.width) heights.append(sizelayout.height) return max(widths), max(heights) # Old text layout functions, to be deprecated. def set_font(self, fontface, size, antialias=True): self.ctx.select_font_face(fontface) self.ctx.set_font_size(size) fo = self.ctx.get_font_options() fo.set_antialias(cairocffi.ANTIALIAS_SUBPIXEL) def text_extents(self, text): return self.ctx.text_extents(utils.scrub_to_utf8(text)) def font_extents(self): return self.ctx.font_extents() def fit_fontsize(self, heightlimit): """Try to find a maximum font size that fits any strings within the height""" self.ctx.set_font_size(heightlimit) asc, desc, height, _, _ = self.font_extents() self.ctx.set_font_size( int(heightlimit * heightlimit / height)) return self.font_extents() def fit_text(self, strings, heightlimit): """Try to find a maximum font size that fits all strings within the height""" self.ctx.set_font_size(heightlimit) _, _, _, maxheight, _, _ = self.ctx.text_extents("".join(strings)) if not maxheight: return 0, 0 self.ctx.set_font_size( int(heightlimit * heightlimit / maxheight)) maxwidth, maxheight = 0, 0 for i in strings: _, _, x, y, _, _ = self.ctx.text_extents(i) maxwidth = max(maxwidth, x) maxheight = max(maxheight, y) return maxwidth, maxheight def draw_vbar(self, color, x, y1, y2, linewidth=1): self.set_source_rgb(color) self.ctx.move_to(x, y1) self.ctx.line_to(x, y2) self.ctx.set_line_width(linewidth) self.ctx.stroke() def draw_hbar(self, color, x1, x2, y, linewidth=1): self.set_source_rgb(color) self.ctx.move_to(x1, y) self.ctx.line_to(x2, y) self.ctx.set_line_width(linewidth) self.ctx.stroke() qtile-0.10.7/libqtile/extention/000077500000000000000000000000001305063162100165345ustar00rootroot00000000000000qtile-0.10.7/libqtile/extention/__init__.py000066400000000000000000000022411305063162100206440ustar00rootroot00000000000000# Copyright (c) 2016 zordsdavini # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from .dmenu import Dmenu, DmenuRun # noqa from .window_list import WindowList # noqa qtile-0.10.7/libqtile/extention/dmenu.py000066400000000000000000000112341305063162100202170ustar00rootroot00000000000000# Copyright (C) 2016, zordsdavini # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import six from subprocess import Popen, PIPE class Dmenu(): """ Python wrapper for dmenu http://tools.suckless.org/dmenu/ config.py should have something like: from libqtile import extention mod = 'mod4' keys = [ ... Key([mod], 'l', lazy.run_extention(extention.WindowList)), Key([mod], 'r', lazy.run_extention(extention.DmenuRun)), ... ] extentions = { 'dmenu': { 'prompt': "→", 'font' : "Andika-8", 'background' : "#15181a", 'foreground' : "#00ff00", 'selected_background' : "#079822", 'selected_foreground' : "#fff", 'height' : 24, } } """ defaults = [ ("bottom", False, "dmenu appears at the bottom of the screen"), ("ignorecase", False, "dmenu matches menu items case insensitively"), ("lines", None, "dmenu lists items vertically, with the given number of lines"), ("prompt", None, "defines the prompt to be displayed to the left of the input field"), ("font", None, "defines the font or font set used"), ("background", None, "defines the normal background color"), ("foreground", None, "defines the normal foreground color"), ("selected_background", None, "defines the selected background color"), ("selected_foreground", None, "defines the selected foreground color"), ("height", None, "defines the height"), ] args = [] lines = [] def __init__(self, config, lines=None): default_config = dict((d[0], d[1]) for d in self.defaults) default_config.update(config) self.configure(default_config, lines) def configure(self, config, lines): self.lines = [] if 'lines' in config and config['lines']: self.lines = ["-l", str(config['lines'])] if lines: self.lines = ["-l", str(lines)] if 'bottom' in config and config['bottom']: self.args.append("-b") if 'ignorecase' in config and config['ignorecase']: self.args.append("-i") if 'prompt' in config and config['prompt']: self.args.extend(("-p", config['prompt'])) if 'font' in config and config['font']: self.args.extend(("-fn", config['font'])) if 'background' in config and config['background']: self.args.extend(("-nb", config['background'])) if 'foreground' in config and config['foreground']: self.args.extend(("-nf", config['foreground'])) if 'selected_background' in config and config['selected_background']: self.args.extend(("-sb", config['selected_background'])) if 'selected_foreground' in config and config['selected_foreground']: self.args.extend(("-sf", config['selected_foreground'])) if 'height' in config and config['height']: self.args.extend(("-h", str(config['height']))) def call(self, items=[]): input_str = "\n".join([six.u(i) for i in items]) + "\n" proc = Popen(["dmenu"] + self.args + self.lines, stdout=PIPE, stdin=PIPE) return proc.communicate(str.encode(input_str))[0] def run_apps(self): Popen(["dmenu_run"] + self.args + self.lines, stdout=PIPE, stdin=PIPE) class DmenuRun(): """ Special case to run applications. """ config = {} def __init__(self, qtile): if hasattr(qtile.config, 'extentions') and qtile.config.extentions['dmenu']: self.config = qtile.config.extentions['dmenu'] def run(self): dmenu = Dmenu(self.config) dmenu.run_apps() qtile-0.10.7/libqtile/extention/window_list.py000066400000000000000000000042731305063162100214560ustar00rootroot00000000000000# Copyright (C) 2016, zordsdavini # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import re from .dmenu import Dmenu class WindowList(): """ Give vertical list of all open windows in dmenu. Switch to selected. """ config = {} qtile = None wins = [] id_map = {} def __init__(self, qtile): self.qtile = qtile if hasattr(qtile.config, 'extentions') and qtile.config.extentions['dmenu']: self.config = qtile.config.extentions['dmenu'] def get_windows(self): id = 0 self.wins = [] self.id_map = {} for win in self.qtile.windowMap.values(): if win.group: self.wins.append("%i: %s (%s)" % (id, win.name, win.group.name)) self.id_map[id] = win id = id + 1 return id def run(self): win_count = self.get_windows() config_tmp = self.config dmenu = Dmenu(config_tmp, win_count) out = dmenu.call(self.wins) if not out: return id = int(re.match(b"^\d+", out).group()) win = self.id_map[id] screen = self.qtile.currentScreen screen.setGroup(win.group) win.group.focus(win) qtile-0.10.7/libqtile/ffi_build.py000066400000000000000000000131351305063162100170170ustar00rootroot00000000000000# Copyright (c) 2014-2015 Sean Vig # Copyright (c) 2014 roger # Copyright (c) 2014 Tycho Andersen # Copyright (c) 2015 Craig Barnes # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from cffi import FFI from xcffib.ffi_build import ffi as xcffib_ffi from cairocffi.ffi_build import ffi as cairocffi_ffi pango_ffi = FFI() # PyPy < 2.6 compatibility if hasattr(pango_ffi, 'set_source'): pango_ffi.set_source("libqtile._ffi_pango", None) pango_ffi.include(cairocffi_ffi) pango_ffi.cdef(""" typedef ... PangoContext; typedef ... PangoLayout; typedef ... PangoFontDescription; typedef ... PangoAttrList; typedef enum { PANGO_ALIGN_LEFT, PANGO_ALIGN_CENTER, PANGO_ALIGN_RIGHT } PangoAlignment; typedef enum { PANGO_ELLIPSEIZE_NONE, PANGO_ELLIPSIZE_START, PANGO_ELLIPSIZE_MIDDLE, PANGO_ELLIPSIZE_END } PangoEllipsizeMode; int pango_units_from_double (double d); typedef void* gpointer; typedef int gboolean; typedef unsigned int guint32; typedef guint32 gunichar; typedef char gchar; typedef signed long gssize; typedef ... GError; typedef int gint; void pango_cairo_show_layout (cairo_t *cr, PangoLayout *layout); gboolean pango_parse_markup (const char *markup_text, int length, gunichar accel_marker, PangoAttrList **attr_list, char **text, gunichar *accel_char, GError **error); // https://developer.gnome.org/pango/stable/pango-Layout-Objects.html PangoLayout *pango_cairo_create_layout (cairo_t *cr); void g_object_unref(gpointer object); void pango_layout_set_font_description (PangoLayout *layout, const PangoFontDescription *desc); const PangoFontDescription * pango_layout_get_font_description (PangoLayout *layout); void pango_layout_set_alignment (PangoLayout *layout, PangoAlignment alignment); void pango_layout_set_attributes (PangoLayout *layout, PangoAttrList *attrs); void pango_layout_set_text (PangoLayout *layout, const char *text, int length); const char * pango_layout_get_text (PangoLayout *layout); void pango_layout_get_pixel_size (PangoLayout *layout, int *width, int *height); void pango_layout_set_width (PangoLayout *layout, int width); void pango_layout_set_ellipsize (PangoLayout *layout, PangoEllipsizeMode ellipsize); PangoEllipsizeMode pango_layout_get_ellipsize (PangoLayout *layout); // https://developer.gnome.org/pango/stable/pango-Fonts.html#PangoFontDescription PangoFontDescription *pango_font_description_new (void); void pango_font_description_free (PangoFontDescription *desc); PangoFontDescription * pango_font_description_from_string (const char *str); void pango_font_description_set_family (PangoFontDescription *desc, const char *family); const char * pango_font_description_get_family (const PangoFontDescription *desc); void pango_font_description_set_absolute_size (PangoFontDescription *desc, double size); void pango_font_description_set_size (PangoFontDescription *desc, gint size); gint pango_font_description_get_size (const PangoFontDescription *desc); // https://developer.gnome.org/glib/stable/glib-Simple-XML-Subset-Parser.html gchar * g_markup_escape_text(const gchar *text, gssize length); """) xcursors_ffi = FFI() # PyPy < 2.6 compatibility if hasattr(xcursors_ffi, 'set_source'): xcursors_ffi.set_source("libqtile._ffi_xcursors", None) xcursors_ffi.include(xcffib_ffi) xcursors_ffi.cdef(""" typedef uint32_t xcb_cursor_t; typedef struct xcb_cursor_context_t xcb_cursor_context_t; int xcb_cursor_context_new( xcb_connection_t *conn, xcb_screen_t *screen, xcb_cursor_context_t **ctx ); xcb_cursor_t xcb_cursor_load_cursor( xcb_cursor_context_t *ctx, const char *name ); void xcb_cursor_context_free(xcb_cursor_context_t *ctx); """) if __name__ == "__main__": pango_ffi.compile() xcursors_ffi.compile() qtile-0.10.7/libqtile/group.py000066400000000000000000000364171305063162100162400ustar00rootroot00000000000000# Copyright (c) 2012-2014 Tycho Andersen # Copyright (c) 2013 xarvh # Copyright (c) 2013 roger # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 dequis # Copyright (c) 2015 Dario Giovannetti # Copyright (c) 2015 Alexander Lozovskoy # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import contextlib import xcffib import xcffib.xproto from . import command from . import hook from . import window from . import utils from .log_utils import logger class _Group(command.CommandObject): """A container for a bunch of windows Analogous to workspaces in other window managers. Each client window managed by the window manager belongs to exactly one group. """ def __init__(self, name, layout=None): self.name = name self.customLayout = layout # will be set on _configure self.windows = set() self.qtile = None self.layouts = [] self.floating_layout = None # self.focusHistory lists the group's windows in the order they # received focus, from the oldest (first item) to the currently # focused window (last item); NB the list does *not* contain any # windows that never received focus; refer to self.windows for the # complete set self.focusHistory = [] self.screen = None self.currentLayout = None def _configure(self, layouts, floating_layout, qtile): self.screen = None self.currentLayout = 0 self.focusHistory = [] self.windows = set() self.qtile = qtile self.layouts = [i.clone(self) for i in layouts] self.floating_layout = floating_layout if self.customLayout is not None: self.layout = self.customLayout self.customLayout = None @property def currentWindow(self): try: return self.focusHistory[-1] except IndexError: # no window has focus return None @currentWindow.setter def currentWindow(self, win): try: self.focusHistory.remove(win) except ValueError: # win has never received focus before pass self.focusHistory.append(win) def _remove_from_focus_history(self, win): try: index = self.focusHistory.index(win) except ValueError: # win has never received focus return False else: del self.focusHistory[index] # return True if win was the last item (i.e. it was currentWindow) return index == len(self.focusHistory) @property def layout(self): return self.layouts[self.currentLayout] @layout.setter def layout(self, layout): """ Parameters ========== layout : a string with matching the name of a Layout object. """ for index, obj in enumerate(self.layouts): if obj.name == layout: self.currentLayout = index hook.fire( "layout_change", self.layouts[self.currentLayout], self ) self.layoutAll() return raise ValueError("No such layout: %s" % layout) def toLayoutIndex(self, index): assert 0 <= index < len(self.layouts), "layout index out of bounds" self.layout.hide() self.currentLayout = index hook.fire("layout_change", self.layouts[self.currentLayout], self) self.layoutAll() screen = self.screen.get_rect() self.layout.show(screen) def nextLayout(self): self.toLayoutIndex((self.currentLayout + 1) % (len(self.layouts))) def prevLayout(self): self.toLayoutIndex((self.currentLayout - 1) % (len(self.layouts))) def layoutAll(self, warp=False): """Layout the floating layer, then the current layout. If we have have a currentWindow give it focus, optionally moving warp to it. """ if self.screen and len(self.windows): with self.disableMask(xcffib.xproto.EventMask.EnterWindow): normal = [x for x in self.windows if not x.floating] floating = [ x for x in self.windows if x.floating and not x.minimized ] screen = self.screen.get_rect() if normal: try: self.layout.layout(normal, screen) except: logger.exception("Exception in layout %s", self.layout.name) if floating: self.floating_layout.layout(floating, screen) if self.currentWindow and \ self.screen == self.qtile.currentScreen: self.currentWindow.focus(warp) def _setScreen(self, screen): """Set this group's screen to new_screen""" if screen == self.screen: return self.screen = screen if self.screen: # move all floating guys offset to new screen self.floating_layout.to_screen(self, self.screen) self.layoutAll() rect = self.screen.get_rect() self.floating_layout.show(rect) self.layout.show(rect) else: self.hide() def hide(self): self.screen = None with self.disableMask(xcffib.xproto.EventMask.EnterWindow | xcffib.xproto.EventMask.FocusChange | xcffib.xproto.EventMask.LeaveWindow): for i in self.windows: i.hide() self.layout.hide() @contextlib.contextmanager def disableMask(self, mask): for i in self.windows: i._disableMask(mask) yield for i in self.windows: i._resetMask() def focus(self, win, warp=True, force=False): """Focus the given window If win is in the group, blur any windows and call ``focus`` on the layout (in case it wants to track anything), fire focus_change hook and invoke layoutAll. Parameters ========== win : Window to focus warp : Warp pointer to win. This should basically always be True, unless the focus event is coming from something like EnterNotify, where the user is actively using the mouse, or on full screen layouts where only one window is "maximized" at a time, and it doesn't make sense for the mouse to automatically move. """ if self.qtile._drag and not force: # don't change focus while dragging windows (unless forced) return if win: if win not in self.windows: return self.currentWindow = win if win.floating: for l in self.layouts: l.blur() self.floating_layout.focus(win) else: self.floating_layout.blur() for l in self.layouts: l.focus(win) hook.fire("focus_change") # !!! note that warp isn't hooked up now self.layoutAll(warp) def info(self): return dict( name=self.name, focus=self.currentWindow.name if self.currentWindow else None, windows=[i.name for i in self.windows], focusHistory=[i.name for i in self.focusHistory], layout=self.layout.name, layouts=[l.name for l in self.layouts], floating_info=self.floating_layout.info(), screen=self.screen.index if self.screen else None ) def add(self, win, focus=True, force=False): hook.fire("group_window_add") self.windows.add(win) win.group = self try: if 'fullscreen' in win.window.get_net_wm_state() and \ self.qtile.config.auto_fullscreen: win._float_state = window.FULLSCREEN elif self.floating_layout.match(win): # !!! tell it to float, can't set floating # because it's too early # so just set the flag underneath win._float_state = window.FLOATING except (xcffib.xproto.WindowError, xcffib.xproto.AccessError): pass # doesn't matter if win.floating: self.floating_layout.add(win) else: for i in self.layouts: i.add(win) if focus: self.focus(win, warp=True, force=force) def remove(self, win, force=False): self.windows.remove(win) hadfocus = self._remove_from_focus_history(win) win.group = None if win.floating: nextfocus = self.floating_layout.remove(win) nextfocus = nextfocus or \ self.currentWindow or \ self.layout.focus_first() or \ self.floating_layout.focus_first(group=self) else: for i in self.layouts: if i is self.layout: nextfocus = i.remove(win) else: i.remove(win) nextfocus = nextfocus or \ self.floating_layout.focus_first(group=self) or \ self.currentWindow or \ self.layout.focus_first() # a notification may not have focus if hadfocus: self.focus(nextfocus, warp=True, force=force) # no next focus window means focus changed to nothing if not nextfocus: hook.fire("focus_change") elif self.screen: self.layoutAll() def mark_floating(self, win, floating): if floating: if win in self.floating_layout.find_clients(self): # already floating pass else: for i in self.layouts: i.remove(win) if win is self.currentWindow: i.blur() self.floating_layout.add(win) if win is self.currentWindow: self.floating_layout.focus(win) else: self.floating_layout.remove(win) self.floating_layout.blur() for i in self.layouts: i.add(win) if win is self.currentWindow: i.focus(win) self.layoutAll() def _items(self, name): if name == "layout": return (True, list(range(len(self.layouts)))) elif name == "window": return (True, [i.window.wid for i in self.windows]) elif name == "screen": return (True, None) def _select(self, name, sel): if name == "layout": if sel is None: return self.layout else: return utils.lget(self.layouts, sel) elif name == "window": if sel is None: return self.currentWindow else: for i in self.windows: if i.window.wid == sel: return i elif name == "screen": return self.screen def cmd_setlayout(self, layout): self.layout = layout def cmd_info(self): """Returns a dictionary of info for this group""" return self.info() def cmd_toscreen(self, screen=None): """Pull a group to a specified screen. Parameters ========== screen : Screen offset. If not specified, we assume the current screen. Examples ======== Pull group to the current screen:: toscreen() Pull group to screen 0:: toscreen(0) """ if screen is None: screen = self.qtile.currentScreen else: screen = self.qtile.screens[screen] screen.setGroup(self) def _dirGroup(self, direction, skip_empty=False, skip_managed=False): """Find a group walking the groups list in the specified direction Parameters ========== skip_empty : skips the empty groups skip_managed : skips the groups that have a screen """ def match(group): if group is self: return True if skip_empty and not group.windows: return False if skip_managed and group.screen: return False return True groups = [group for group in self.qtile.groups if match(group)] index = (groups.index(self) + direction) % len(groups) return groups[index] def prevGroup(self, skip_empty=False, skip_managed=False): return self._dirGroup(-1, skip_empty, skip_managed) def nextGroup(self, skip_empty=False, skip_managed=False): return self._dirGroup(1, skip_empty, skip_managed) def cmd_unminimize_all(self): """Unminimise all windows in this group""" for w in self.windows: w.minimized = False self.layoutAll() def cmd_next_window(self): if not self.windows: return if self.currentWindow.floating: nxt = self.floating_layout.focus_next(self.currentWindow) or \ self.layout.focus_first() or \ self.floating_layout.focus_first(group=self) else: nxt = self.layout.focus_next(self.currentWindow) or \ self.floating_layout.focus_first(group=self) or \ self.layout.focus_first() self.focus(nxt, True) def cmd_prev_window(self): if not self.windows: return if self.currentWindow.floating: nxt = self.floating_layout.focus_previous(self.currentWindow) or \ self.layout.focus_last() or \ self.floating_layout.focus_last(group=self) else: nxt = self.layout.focus_previous(self.currentWindow) or \ self.floating_layout.focus_last(group=self) or \ self.layout.focus_last() self.focus(nxt, True) def cmd_switch_groups(self, name): """Switch position of current group with name""" self.qtile.cmd_switch_groups(self.name, name) def __repr__(self): return "" % self.name qtile-0.10.7/libqtile/hook.py000066400000000000000000000227551305063162100160440ustar00rootroot00000000000000# Copyright (c) 2009-2010 Aldo Cortesi # Copyright (c) 2010 Lee McCuller # Copyright (c) 2010 matt # Copyright (c) 2010, 2014 dequis # Copyright (c) 2010, 2012, 2014 roger # Copyright (c) 2011 Florian Mounier # Copyright (c) 2011 Kenji_Takahashi # Copyright (c) 2011 Paul Colomiets # Copyright (c) 2011 Tzbob # Copyright (c) 2012-2015 Tycho Andersen # Copyright (c) 2012 Craig Barnes # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 Sean Vig # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from .log_utils import logger from . import utils subscriptions = {} SKIPLOG = set() qtile = None def init(q): global qtile qtile = q def clear(): subscriptions.clear() class Subscribe(object): def __init__(self): hooks = set([]) for i in dir(self): if not i.startswith("_"): hooks.add(i) self.hooks = hooks def _subscribe(self, event, func): lst = subscriptions.setdefault(event, []) if func not in lst: lst.append(func) def startup_once(self, func): """Called when Qtile has started on first start This hook is called exactly once per session (i.e. not on each ``lazy.restart()``). **Arguments** None """ return self._subscribe("startup_once", func) def startup(self, func): """Called when qtile is started **Arguments** None """ return self._subscribe("startup", func) def startup_complete(self, func): """Called when qtile is started after all resources initialized **Arguments** None """ return self._subscribe("startup_complete", func) def setgroup(self, func): """Called when group is changed **Arguments** None """ return self._subscribe("setgroup", func) def addgroup(self, func): """Called when group is added **Arguments** * qtile manager instance * name of new group """ return self._subscribe("addgroup", func) def delgroup(self, func): """Called when group is deleted **Arguments** * qtile manager instance * name of deleted group """ return self._subscribe("delgroup", func) def changegroup(self, func): """Called whenever a group change occurs **Arguments** None """ return self._subscribe("changegroup", func) def focus_change(self, func): """Called when focus is changed **Arguments** None """ return self._subscribe("focus_change", func) def float_change(self, func): """Called when a change in float state is made **Arguments** None """ return self._subscribe("float_change", func) def group_window_add(self, func): """Called when a new window is added to a group **Arguments** None """ return self._subscribe("group_window_add", func) def window_name_change(self, func): """Called whenever a windows name changes **Arguments** None """ return self._subscribe("window_name_change", func) def client_new(self, func): """Called before Qtile starts managing a new client Use this hook to declare windows static, or add them to a group on startup. This hook is not called for internal windows. **Arguments** * ``window.Window`` object Examples -------- :: @libqtile.hook.subscribe.client_new def func(c): if c.name == "xterm": c.togroup("a") elif c.name == "dzen": c.static(0) """ return self._subscribe("client_new", func) def client_managed(self, func): """Called after Qtile starts managing a new client Called after a window is assigned to a group, or when a window is made static. This hook is not called for internal windows. **Arguments** * ``window.Window`` object of the managed window """ return self._subscribe("client_managed", func) def client_killed(self, func): """Called after a client has been unmanaged **Arguments** * ``window.Window`` object of the killed window. """ return self._subscribe("client_killed", func) def client_state_changed(self, func): """Called whenever client state changes Never fires """ return self._subscribe("client_state_changed", func) def client_type_changed(self, func): """Called whenever window type changes Never fires """ return self._subscribe("client_type_changed", func) def client_focus(self, func): """Called whenever focus changes **Arguments** * ``window.Window`` object of the new focus. """ return self._subscribe("client_focus", func) def client_mouse_enter(self, func): """Called when the mouse enters a client **Arguments** * ``window.Window`` of window entered """ return self._subscribe("client_mouse_enter", func) def client_name_updated(self, func): """Called when the client name changes Never fires """ return self._subscribe("client_name_updated", func) def client_urgent_hint_changed(self, func): """Called when the client urgent hint changes **Arguments** * ``window.Window`` of client with hint change """ return self._subscribe("client_urgent_hint_changed", func) def layout_change(self, func): """Called on layout change **Arguments** * layout object for new layout * group object on which layout is changed """ return self._subscribe("layout_change", func) def net_wm_icon_change(self, func): """Called on `_NET_WM_ICON` chance **Arguments** * ``window.Window`` of client with changed icon """ return self._subscribe("net_wm_icon_change", func) def selection_notify(self, func): """Called on selection notify **Arguments** * name of the selection * dictionary describing selection, containing ``owner`` and ``selection`` as keys """ return self._subscribe("selection_notify", func) def selection_change(self, func): """Called on selection change **Arguments** * name of the selection * dictionary describing selection, containing ``owner`` and ``selection`` as keys """ return self._subscribe("selection_change", func) def screen_change(self, func): """Called when a screen is added or screen configuration is changed (via xrandr) Common usage is simply to call ``qtile.cmd_restart()`` on each event (to restart qtile when there is a new monitor): **Arguments** * qtile manager instance * ``xproto.randr.ScreenChangeNotify`` event Examples -------- :: @libqtile.hook.subscribe.screen_change def restart_on_randr(qtile, ev): qtile.cmd_restart() """ return self._subscribe("screen_change", func) def current_screen_change(self, func): """Called when the current screen (i.e. the screen with focus) changes **Arguments** None """ return self._subscribe("current_screen_change", func) subscribe = Subscribe() class Unsubscribe(Subscribe): """ This class mirrors subscribe, except the _subscribe member has been overridden to removed calls from hooks. """ def _subscribe(self, event, func): lst = subscriptions.setdefault(event, []) try: lst.remove(func) except ValueError: raise utils.QtileError( "Tried to unsubscribe a hook that was not" " currently subscribed" ) unsubscribe = Unsubscribe() def fire(event, *args, **kwargs): if event not in subscribe.hooks: raise utils.QtileError("Unknown event: %s" % event) if event not in SKIPLOG: logger.info("Internal event: %s(%s, %s)", event, args, kwargs) for i in subscriptions.get(event, []): try: i(*args, **kwargs) except: logger.exception("Error in hook %s", event) qtile-0.10.7/libqtile/interactive/000077500000000000000000000000001305063162100170345ustar00rootroot00000000000000qtile-0.10.7/libqtile/interactive/__init__.py000066400000000000000000000000001305063162100211330ustar00rootroot00000000000000qtile-0.10.7/libqtile/interactive/iqshell_install.py000066400000000000000000000040221305063162100225730ustar00rootroot00000000000000# Copyright (c) 2016 Sean Vig # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import json import os import sys from jupyter_client.kernelspec import install_kernel_spec # On Python 3, TemporaryDirectory works with context manager try: from tempfile import TemporaryDirectory except ImportError: from IPython.utils.tempdir import TemporaryDirectory kernel_json = { "argv": [sys.executable, "-m", "libqtile.interactive.iqshell_kernel", "-f", "{connection_file}"], "display_name": "iqshell", "language": "qshell", } def main(argv=None): if argv is None: argv = [] user = '--user' in argv or os.geteuid() != 0 with TemporaryDirectory() as td: # IPython tempdir starts off as 700, not user readable os.chmod(td, 0o755) with open(os.path.join(td, 'kernel.json'), 'w') as f: json.dump(kernel_json, f, sort_keys=True) print('Installing IPython kernel spec') install_kernel_spec(td, 'qshell', user=user, replace=True) if __name__ == '__main__': main(sys.argv) qtile-0.10.7/libqtile/interactive/iqshell_kernel.py000066400000000000000000000065471305063162100224230ustar00rootroot00000000000000# Copyright (c) 2016, Sean Vig # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from libqtile import sh, command from ipykernel.kernelbase import Kernel class QshKernel(Kernel): implementation = 'qshell' implementation_version = '0.1' language = 'no-op' language_version = '1.0' language_info = {'mimetype': 'text/plain'} banner = "Qsh Kernel" def __init__(self, **kwargs): Kernel.__init__(self, **kwargs) self.client = command.Client() self.qsh = sh.QSh(self.client) def do_execute(self, code, silent, store_history=True, user_expressions=None, allow_stdin=False): # if no command sent, just return if not code.strip(): return { 'status': 'ok', 'execution_count': self.execution_count, 'payload': [], 'user_expressions': {}, } if code[-1] == '?': return self.do_inspect(code, len(code) - 1) try: output = self.qsh.process_command(code) except KeyboardInterrupt: return { 'status': 'abort', 'execution_count': self.execution_count, } if not silent and output: stream_content = {'name': 'stdout', 'text': output} self.send_response(self.iopub_socket, 'stream', stream_content) return { 'status': 'ok', 'execution_count': self.execution_count, 'payload': [], 'user_expressions': {}, } def do_complete(self, code, cursor_pos): no_complete = { 'status': 'ok', 'matches': [], 'cursor_start': 0, 'cursor_end': cursor_pos, 'metadata': dict(), } if not code or code[-1] == ' ': return no_complete tokens = code.split() if not tokens: return no_complete token = tokens[-1] start = cursor_pos - len(token) matches = self.qsh._complete(code, token) return { 'status': 'ok', 'matches': sorted(matches), 'cursor_start': start, 'cursor_end': cursor_pos, 'metadata': dict(), } def main(): from ipykernel.kernelapp import IPKernelApp IPKernelApp.launch_instance(kernel_class=QshKernel) if __name__ == '__main__': main() qtile-0.10.7/libqtile/ipc.py000066400000000000000000000172151305063162100156520ustar00rootroot00000000000000# Copyright (c) 2008, Aldo Cortesi. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """ A simple IPC mechanism for communicating between two local processes. We use marshal to serialize data - this means that both client and server must run the same Python version, and that clients must be trusted (as un-marshalling untrusted data can result in arbitrary code execution). """ import marshal import os.path import socket import struct import fcntl import json from . import asyncio from .log_utils import logger HDRLEN = 4 class IPCError(Exception): pass class _IPC(object): def _unpack(self, data): if data is None: raise IPCError("received data is None") try: return json.loads(data.decode('utf-8')), True except ValueError: pass try: assert len(data) >= HDRLEN size = struct.unpack("!L", data[:HDRLEN])[0] assert size >= len(data[HDRLEN:]) return self._unpack_body(data[HDRLEN:HDRLEN + size]), False except AssertionError: raise IPCError( "error reading reply!" " (probably the socket was disconnected)" ) @staticmethod def _unpack_body(body): return marshal.loads(body) @staticmethod def _pack_json(msg): json_obj = json.dumps(msg) return json_obj.encode('utf-8') @staticmethod def _pack(msg): msg = marshal.dumps(msg) size = struct.pack("!L", len(msg)) return size + msg class _ClientProtocol(asyncio.Protocol, _IPC): """IPC Client Protocol 1. Once the connection is made, the client initializes a Future self.reply, which will hold the response from the server. 2. The message is sent to the server with .send(msg), which closes the connection once the message is sent. 3. The client then receives data from the server until the server closes the connection, signalling that all the data has been sent. 4. When the server sends on EOF, the data is unpacked and stored to the reply future. """ def connection_made(self, transport): self.transport = transport self.recv = b'' self.reply = asyncio.Future() def send(self, msg, is_json=False): if is_json: send_data = self._pack_json(msg) else: send_data = self._pack(msg) self.transport.write(send_data) try: self.transport.write_eof() except AttributeError: logger.exception('Swallowing AttributeError due to asyncio bug!') def data_received(self, data): self.recv += data def eof_received(self): # The server sends EOF when there is data ready to be processed try: data, _ = self._unpack(self.recv) except IPCError as e: self.reply.set_exception(e) else: self.reply.set_result(data) def connection_lost(self, exc): # The client shouldn't just lose the connection without an EOF if exc: self.reply.set_exception(exc) if not self.reply.done(): self.reply.set_exception(IPCError) class Client(object): def __init__(self, fname, is_json=False): self.fname = fname self.loop = asyncio.get_event_loop() self.is_json = is_json def send(self, msg): client_coroutine = self.loop.create_unix_connection(_ClientProtocol, path=self.fname) try: _, client_proto = self.loop.run_until_complete(client_coroutine) except OSError: raise IPCError("Could not open %s" % self.fname) client_proto.send(msg, is_json=self.is_json) try: self.loop.run_until_complete(asyncio.wait_for(client_proto.reply, timeout=10)) except asyncio.TimeoutError: raise RuntimeError("Server not responding") return client_proto.reply.result() def call(self, data): return self.send(data) class _ServerProtocol(asyncio.Protocol, _IPC): """IPC Server Protocol 1. The server is initialized with a handler callback function for evaluating incoming queries. 2. Once the connection is made, the server initializes a data store for incoming data. 3. The client sends all its data to the server, which is stored. 4. The client signals that all data is sent by sending an EOF, at which point the server then unpacks the data and runs it through the handler. The result is returned to the client and the connection is closed. """ def __init__(self, handler): asyncio.Protocol.__init__(self) self.handler = handler self.transport = None self.data = None def connection_made(self, transport): self.transport = transport logger.info('Connection made to server') self.data = b'' def data_received(self, recv): logger.info('Data received by server') self.data += recv def eof_received(self): logger.info('EOF received by server') try: req, is_json = self._unpack(self.data) except IPCError: logger.warn('Invalid data received, closing connection') self.transport.close() return finally: self.data = None if req[1] == 'restart': logger.info('Closing connection on restart') self.transport.write_eof() rep = self.handler(req) if is_json: result = self._pack_json(rep) else: result = self._pack(rep) logger.info('Sending result on receive EOF') self.transport.write(result) logger.info('Closing connection on receive EOF') self.transport.write_eof() class Server(object): def __init__(self, fname, handler, loop): self.fname = fname self.handler = handler self.loop = loop self.server = None if os.path.exists(fname): os.unlink(fname) self.sock = socket.socket( socket.AF_UNIX, socket.SOCK_STREAM, 0 ) flags = fcntl.fcntl(self.sock, fcntl.F_GETFD) fcntl.fcntl(self.sock, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC) self.sock.bind(self.fname) def close(self): logger.info('Stopping server on server close') self.server.close() self.sock.close() def start(self): serverprotocol = _ServerProtocol(self.handler) server_coroutine = self.loop.create_unix_server(lambda: serverprotocol, sock=self.sock, backlog=5) logger.info('Starting server') self.server = self.loop.run_until_complete(server_coroutine) qtile-0.10.7/libqtile/layout/000077500000000000000000000000001305063162100160345ustar00rootroot00000000000000qtile-0.10.7/libqtile/layout/__init__.py000066400000000000000000000032221305063162100201440ustar00rootroot00000000000000# Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Florian Scherf # Copyright (c) 2014 Tycho Andersen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # https://bitbucket.org/tarek/flake8/issue/141/improve-flake8-statement-to-ignore # is annoying, so we ignore libqtile/layout/__init__.py completely # flake8: noqa from .stack import Stack from .max import Max from .xmonad import MonadTall from .tile import Tile from .floating import Floating from .ratiotile import RatioTile from .slice import Slice from .tree import TreeTab from .zoomy import Zoomy from .matrix import Matrix from .verticaltile import VerticalTile from .wmii import Wmii from .columns import Columns qtile-0.10.7/libqtile/layout/base.py000066400000000000000000000210411305063162100173160ustar00rootroot00000000000000# Copyright (c) 2008, Aldo Cortesi. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import copy import six from abc import ABCMeta, abstractmethod from .. import command, configurable @six.add_metaclass(ABCMeta) class Layout(command.CommandObject, configurable.Configurable): """This class defines the API that should be exposed by all layouts""" @classmethod def _name(cls): return cls.__class__.__name__.lower() defaults = [( "name", None, "The name of this layout" " (usually the class' name in lowercase, e.g. 'max')" )] def __init__(self, **config): # name is a little odd; we can't resolve it until the class is defined # (i.e., we can't figure it out to define it in Layout.defaults), so # we resolve it here instead. if "name" not in config: config["name"] = self.__class__.__name__.lower() command.CommandObject.__init__(self) configurable.Configurable.__init__(self, **config) self.add_defaults(Layout.defaults) def layout(self, windows, screen): assert windows, "let's eliminate unnecessary calls" for i in windows: self.configure(i, screen) def finalize(self): pass def clone(self, group): """Duplicate a layout Make a copy of this layout. This is done to provide each group with a unique instance of every layout. Parameters ========== group: Group to attach new layout instance to. """ c = copy.copy(self) c.group = group return c def _items(self, name): if name == "screen": return (True, None) elif name == "group": return (True, None) def _select(self, name, sel): if name == "screen": return self.group.screen elif name == "group": return self.group def show(self, screen): """Called when layout is being shown""" pass def hide(self): """Called when layout is being hidden""" pass def focus(self, client): """Called whenever the focus changes""" pass def blur(self): """Called whenever focus is gone from this layout""" pass def info(self): """Returns a dictionary of layout information""" return dict( name=self.name, group=self.group.name if self.group else None ) def cmd_info(self): """Return a dictionary of info for this object""" return self.info() @abstractmethod def add(self, client): """Called whenever a window is added to the group Called whether the layout is current or not. The layout should just add the window to its internal datastructures, without mapping or configuring. """ pass @abstractmethod def remove(self, client): """Called whenever a window is removed from the group Called whether the layout is current or not. The layout should just de-register the window from its data structures, without unmapping the window. Returns the "next" window that should gain focus or None. """ pass @abstractmethod def configure(self, client, screen): """Configure the layout This method should: - Configure the dimensions and borders of a window using the `.place()` method. - Call either `.hide()` or `.unhide()` on the window. """ pass @abstractmethod def focus_first(self): pass @abstractmethod def focus_last(self): pass @abstractmethod def focus_next(self, win): pass @abstractmethod def focus_previous(self, win): pass @abstractmethod def cmd_next(self): pass @abstractmethod def cmd_previous(self): pass class SingleWindow(Layout): """Base for layouts with single visible window""" def __init__(self, **config): Layout.__init__(self, **config) @abstractmethod def _get_window(self): """Should return either visible window or None""" pass def configure(self, win, screen): if win is self._get_window(): win.place( screen.x, screen.y, screen.width, screen.height, 0, None, ) win.unhide() else: win.hide() def remove(self, win): cli = self.clients.pop(0) if cli == win: return self.clients[0] class Delegate(Layout): """Base for all delegation layouts""" def __init__(self, **config): self.layouts = {} Layout.__init__(self, **config) def clone(self, group): c = Layout.clone(self, group) c.layouts = {} return c @abstractmethod def _get_layouts(self): """Returns all children layouts""" pass @abstractmethod def _get_active_layout(self): """Returns layout to which delegate commands to""" pass def delegate_layout(self, windows, mapping): """Delegates layouting actual windows Parameterer =========== windows: windows to layout mapping: mapping from layout to ScreenRect for each layout """ grouped = {} for w in windows: lay = self.layouts[w] if lay in grouped: grouped[lay].append(w) else: grouped[lay] = [w] for lay, wins in grouped.items(): lay.layout(wins, mapping[lay]) def remove(self, win): lay = self.layouts.pop(win) focus = lay.remove(win) if not focus: layouts = self._get_layouts() idx = layouts.index(lay) while idx < len(layouts) - 1 and not focus: idx += 1 focus = layouts[idx].focus_first() return focus def focus_first(self): layouts = self._get_layouts() for lay in layouts: win = lay.focus_first() if win: return win def focus_last(self): layouts = self._get_layouts() for lay in reversed(layouts): win = lay.focus_last() if win: return win def focus_next(self, win): layouts = self._get_layouts() cur = self.layouts[win] focus = cur.focus_next(win) if not focus: idx = layouts.index(cur) while idx < len(layouts) - 1 and not focus: idx += 1 focus = layouts[idx].focus_first() return focus def focus_previous(self, win): layouts = self._get_layouts() cur = self.layouts[win] focus = cur.focus_previous(win) if not focus: idx = layouts.index(cur) while idx > 0 and not focus: idx -= 1 focus = layouts[idx].focus_last() return focus def __getattr__(self, name): """Delegate unimplemented command calls to active layout. For ``cmd_``-methods that don't exist on the Delegate subclass, this looks for an implementation on the active layout. """ if name.startswith('cmd_'): return getattr(self._get_active_layout(), name) return super(Delegate, self).__getattr__(name) def info(self): d = Layout.info(self) for layout in self._get_layouts(): d[layout.name] = layout.info() return d qtile-0.10.7/libqtile/layout/columns.py000066400000000000000000000340541305063162100200740ustar00rootroot00000000000000# Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import division from .base import Layout class _Column(object): def __init__(self, autosplit=True, width=100): self.width = width self.split = autosplit self.current = 0 self.clients = [] self.heights = {} def info(self): return dict( clients=[c.name for c in self], heights=[self.heights[c] for c in self], split=self.split, current=self.current, ) @property def cw(self): if len(self): return self.clients[self.current] return None def toggleSplit(self): self.split = not self.split def focus(self, client): self.current = self.index(client) def focus_first(self): if self.split and len(self): return self[0] return self.cw def focus_last(self): if self.split and len(self): return self[-1] return None def focus_next(self, win): idx = self.index(win) + 1 if self.split and idx < len(self): return self[idx] return None def focus_previous(self, win): idx = self.index(win) - 1 if self.split and idx >= 0: return self[idx] return None def add(self, client, height=100): self.clients.insert(self.current, client) self.heights[client] = height delta = 100 - height if delta != 0: n = len(self) growth = [int(delta / n)] * n growth[0] += delta - sum(growth) for c, g in zip(self, growth): self.heights[c] += g def remove(self, client): idx = self.index(client) delta = self.heights[client] - 100 del self.heights[client] del self.clients[idx] if len(self) == 0: self.current = 0 return elif idx <= self.current: self.current = max(0, self.current - 1) if delta != 0: n = len(self) growth = [int(delta / n)] * n growth[0] += delta - sum(growth) for c, g in zip(self, growth): self.heights[c] += g def index(self, client): return self.clients.index(client) def __len__(self): return len(self.clients) def __getitem__(self, i): return self.clients[i] def __setitem__(self, i, c): self.clients[i] = c def __contains__(self, client): return client in self.clients def __str__(self): cur = self.current return "_Column: " + ", ".join([ "[%s: %d]" % (c.name, self.heights[c]) if c == cur else "%s: %d" % (c.name, self.heights[c]) for c in self ]) class Columns(Layout): """Extension of the Stack layout. The screen is split into columns, which can be dynamically added or removed. Each column displays either a sigle window at a time from a stack of windows or all of them simultaneously, spliting the column space. Columns and windows can be resized and windows can be shuffled around. This layout can also emulate "Wmii", "Verical", and "Max", depending on the default parameters. An example key configuration is:: Key([mod], "j", lazy.layout.down()), Key([mod], "k", lazy.layout.up()), Key([mod], "h", lazy.layout.left()), Key([mod], "l", lazy.layout.right()), Key([mod, "shift"], "j", lazy.layout.shuffle_down()), Key([mod, "shift"], "k", lazy.layout.shuffle_up()), Key([mod, "shift"], "h", lazy.layout.shuffle_left()), Key([mod, "shift"], "l", lazy.layout.shuffle_right()), Key([mod, "control"], "j", lazy.layout.grow_down()), Key([mod, "control"], "k", lazy.layout.grow_up()), Key([mod, "control"], "h", lazy.layout.grow_left()), Key([mod, "control"], "l", lazy.layout.grow_right()), Key([mod], "Return", lazy.layout.toggle_split()), Key([mod], "n", lazy.layout.normalize()), """ defaults = [ ("name", "columns", "Name of this layout."), ("border_focus", "#881111", "Border colour for the focused window."), ("border_normal", "#220000", "Border colour for un-focused windows."), ("border_width", 2, "Border width."), ("margin", 0, "Margin of the layout."), ("autosplit", True, "Autosplit newly created columns."), ("num_columns", 2, "Preferred number of columns."), ("grow_amount", 10, "Amount by which to grow a window/column."), ("fair", False, "Add new windows to the column with least windows."), ] def __init__(self, **config): Layout.__init__(self, **config) self.add_defaults(Columns.defaults) self.columns = [_Column(self.autosplit)] self.current = 0 def clone(self, group): c = Layout.clone(self, group) c.columns = [_Column(self.autosplit)] return c def info(self): d = Layout.info(self) d["columns"] = [c.info() for c in self.columns] d["current"] = self.current return d def focus(self, client): for i, c in enumerate(self.columns): if client in c: c.focus(client) self.current = i break @property def cc(self): return self.columns[self.current] def add_column(self, prepend=False): c = _Column(self.autosplit) if prepend: self.columns.insert(0, c) self.current += 1 else: self.columns.append(c) return c def remove_column(self, col): idx = self.columns.index(col) del self.columns[idx] if idx <= self.current: self.current = max(0, self.current - 1) delta = col.width - 100 if delta != 0: n = len(self.columns) growth = [int(delta / n)] * n growth[0] += delta - sum(growth) for c, g in zip(self.columns, growth): c.width += g def add(self, client): c = self.cc if len(c) > 0 and len(self.columns) < self.num_columns: c = self.add_column() if self.fair: least = min(self.columns, key=len) if len(least) < len(c): c = least self.current = self.columns.index(c) c.add(client) def remove(self, client): remove = None for c in self.columns: if client in c: c.remove(client) if len(c) == 0 and len(self.columns) > 1: remove = c break if remove is not None: self.remove_column(c) return self.columns[self.current].cw def configure(self, client, screen): pos = 0 for col in self.columns: if client in col: break pos += col.width else: client.hide() return if client.has_focus: color = self.group.qtile.colorPixel(self.border_focus) else: color = self.group.qtile.colorPixel(self.border_normal) if len(self.columns) == 1 and (len(col) == 1 or not col.split): border = 0 else: border = self.border_width width = int(0.5 + col.width * screen.width * 0.01 / len(self.columns)) x = screen.x + int(0.5 + pos * screen.width * 0.01 / len(self.columns)) if col.split: pos = 0 for c in col: if client == c: break pos += col.heights[c] height = int(0.5 + col.heights[client] * screen.height * 0.01 / len(col)) y = screen.y + int(0.5 + pos * screen.height * 0.01 / len(col)) client.place(x, y, width - 2 * border, height - 2 * border, border, color, margin=self.margin) client.unhide() elif client == col.cw: client.place(x, screen.y, width - 2 * border, screen.height - 2 * border, border, color, margin=self.margin) client.unhide() else: client.hide() def focus_first(self): return self.cc.focus_first() def focus_last(self): return self.cc.focus_last() def focus_next(self, win): for col in self.columns: if win in col: return col.focus_next(win) def focus_previous(self, win): for col in self.columns: if win in col: return col.focus_previous(win) def cmd_toggle_split(self): self.cc.toggleSplit() self.group.layoutAll() def cmd_left(self): if len(self.columns) > 1: self.current = (self.current - 1) % len(self.columns) self.group.focus(self.cc.cw, True) def cmd_right(self): if len(self.columns) > 1: self.current = (self.current + 1) % len(self.columns) self.group.focus(self.cc.cw, True) def cmd_up(self): col = self.cc if len(col) > 1: col.current = (col.current - 1) % len(col) self.group.focus(col.cw, True) def cmd_down(self): col = self.cc if len(col) > 1: col.current = (col.current + 1) % len(col) self.group.focus(col.cw, True) def cmd_next(self): if self.cc.split and self.cc.current < len(self.cc) - 1: self.cc.current += 1 elif self.columns: self.current = (self.current + 1) % len(self.columns) if self.cc.split: self.cc.current = 0 self.group.focus(self.cc.cw, True) def cmd_previous(self): if self.cc.split and self.cc.current > 0: self.cc.current -= 1 elif self.columns: self.current = (self.current - 1) % len(self.columns) if self.cc.split: self.cc.current = len(self.cc) - 1 self.group.focus(self.cc.cw, True) def cmd_shuffle_left(self): cur = self.cc client = cur.cw if client is None: return if self.current > 0: self.current -= 1 new = self.cc new.add(client, cur.heights[client]) cur.remove(client) if len(cur) == 0: self.remove_column(cur) elif len(cur) > 1: new = self.add_column(True) new.add(client, cur.heights[client]) cur.remove(client) self.current = 0 else: return self.group.layoutAll() def cmd_shuffle_right(self): cur = self.cc client = cur.cw if client is None: return if self.current + 1 < len(self.columns): self.current += 1 new = self.cc new.add(client, cur.heights[client]) cur.remove(client) if len(cur) == 0: self.remove_column(cur) elif len(cur) > 1: new = self.add_column() new.add(client, cur.heights[client]) cur.remove(client) self.current = len(self.columns) - 1 else: return self.group.layoutAll() def cmd_shuffle_up(self): col = self.cc if col.current > 0: col[col.current], col[col.current - 1] = \ col[col.current - 1], col[col.current] col.current -= 1 self.group.layoutAll() def cmd_shuffle_down(self): col = self.cc if col.current + 1 < len(col): col[col.current], col[col.current + 1] = \ col[col.current + 1], col[col.current] col.current += 1 self.group.layoutAll() def cmd_grow_left(self): if self.current > 0: if self.columns[self.current - 1].width > self.grow_amount: self.columns[self.current - 1].width -= self.grow_amount self.cc.width += self.grow_amount self.group.layoutAll() def cmd_grow_right(self): if self.current + 1 < len(self.columns): if self.columns[self.current + 1].width > self.grow_amount: self.columns[self.current + 1].width -= self.grow_amount self.cc.width += self.grow_amount self.group.layoutAll() def cmd_grow_up(self): col = self.cc if col.current > 0: if col.heights[col[col.current - 1]] > self.grow_amount: col.heights[col[col.current - 1]] -= self.grow_amount col.heights[col.cw] += self.grow_amount self.group.layoutAll() def cmd_grow_down(self): col = self.cc if col.current + 1 < len(col): if col.heights[col[col.current + 1]] > self.grow_amount: col.heights[col[col.current + 1]] -= self.grow_amount col.heights[col.cw] += self.grow_amount self.group.layoutAll() def cmd_normalize(self): for col in self.columns: for client in col: col.heights[client] = 100 col.width = 100 self.group.layoutAll() qtile-0.10.7/libqtile/layout/floating.py000066400000000000000000000204111305063162100202070ustar00rootroot00000000000000# Copyright (c) 2010 matt # Copyright (c) 2010-2011 Paul Colomiets # Copyright (c) 2011 Mounier Florian # Copyright (c) 2012 Craig Barnes # Copyright (c) 2012, 2014-2015 Tycho Andersen # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2013 Julien Iguchi-Cartigny # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 dequis # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from .base import Layout DEFAULT_FLOAT_WM_TYPES = set([ 'utility', 'notification', 'toolbar', 'splash', 'dialog', ]) DEFAULT_FLOAT_RULES = [ {"role": "About"}, ] class Floating(Layout): """ Floating layout, which does nothing with windows but handles focus order """ defaults = [ ("border_focus", "#0000ff", "Border colour for the focused window."), ("border_normal", "#000000", "Border colour for un-focused windows."), ("border_width", 1, "Border width."), ("max_border_width", 0, "Border width for maximize."), ("fullscreen_border_width", 0, "Border width for fullscreen."), ("name", "floating", "Name of this layout."), ( "auto_float_types", DEFAULT_FLOAT_WM_TYPES, "default wm types to automatically float" ), ] def __init__(self, float_rules=None, **config): """ If you have certain apps that you always want to float you can provide ``float_rules`` to do so. ``float_rules`` is a list of dictionaries containing some or all of the keys:: {'wname': WM_NAME, 'wmclass': WM_CLASS, 'role': WM_WINDOW_ROLE} The keys must be specified as above. You only need one, but you need to provide the value for it. When a new window is opened it's ``match`` method is called with each of these rules. If one matches, the window will float. The following will float gimp and skype:: float_rules=[dict(wmclass="skype"), dict(wmclass="gimp")] Specify these in the ``floating_layout`` in your config. """ Layout.__init__(self, **config) self.clients = [] self.focused = None self.group = None self.float_rules = float_rules or DEFAULT_FLOAT_RULES self.add_defaults(Floating.defaults) def match(self, win): """Used to default float some windows""" if win.window.get_wm_type() in self.auto_float_types: return True for rule_dict in self.float_rules: if win.match(**rule_dict): return True return False def find_clients(self, group): """Find all clients belonging to a given group""" return [c for c in self.clients if c.group is group] def to_screen(self, group, new_screen): """Adjust offsets of clients within current screen""" for win in self.find_clients(group): if win.maximized: win.maximized = True elif win.fullscreen: win.fullscreen = True else: # catch if the client hasn't been configured try: # By default, place window at same offset from top corner new_x = new_screen.x + win.float_x new_y = new_screen.y + win.float_y except AttributeError: # this will be handled in .configure() pass else: # make sure window isn't off screen left/right... new_x = min(new_x, new_screen.x + new_screen.width - win.width) new_x = max(new_x, new_screen.x) # and up/down new_y = min(new_y, new_screen.y + new_screen.height - win.height) new_y = max(new_y, new_screen.y) win.x = new_x win.y = new_y win.group = new_screen.group def focus_first(self, group=None): if group is None: clients = self.clients else: clients = self.find_clients(group) if clients: return clients[0] def focus_next(self, win): if win not in self.clients or win.group is None: return clients = self.find_clients(win.group) idx = clients.index(win) if len(clients) > idx + 1: return clients[idx + 1] def focus_last(self, group=None): if group is None: clients = self.clients else: clients = self.find_clients(group) if clients: return clients[-1] def focus_previous(self, win): if win not in self.clients or win.group is None: return clients = self.find_clients(win.group) idx = clients.index(win) if idx > 0: return clients[idx - 1] def focus(self, client): self.focused = client def blur(self): self.focused = None def configure(self, client, screen): if client.has_focus: bc = client.group.qtile.colorPixel(self.border_focus) else: bc = client.group.qtile.colorPixel(self.border_normal) if client.maximized: bw = self.max_border_width elif client.fullscreen: bw = self.fullscreen_border_width else: bw = self.border_width above = False # We definitely have a screen here, so let's be sure we'll float on screen try: client.float_x client.float_y except AttributeError: # this window hasn't been placed before, let's put it in a sensible spot transient_for = client.window.get_wm_transient_for() win = client.group.qtile.windowMap.get(transient_for) if win is not None: x = win.x + (win.width - client.width) // 2 y = win.y + (win.height - client.height) // 2 above = True else: x = screen.x + client.x % screen.width # try to get right edge on screen (without moving the left edge off) x = min(x, screen.x - client.width) x = max(x, screen.x) # then update it's position (`.place()` will take care of `.float_x`) client.x = x y = screen.y + client.y % screen.height y = min(y, screen.y - client.height) y = max(y, screen.y) client.y = y client.place( client.x, client.y, client.width, client.height, bw, bc, above, ) client.unhide() def add(self, client): self.clients.append(client) self.focused = client def remove(self, client): if client not in self.clients: return next_focus = self.focus_next(client) if client is self.focused: self.blur() self.clients.remove(client) return next_focus def info(self): d = Layout.info(self) d["clients"] = [c.name for c in self.clients] return d def cmd_next(self): # This can't ever be called, but implement the abstract method pass def cmd_previous(self): # This can't ever be called, but implement the abstract method pass qtile-0.10.7/libqtile/layout/matrix.py000066400000000000000000000151261305063162100177170ustar00rootroot00000000000000# Copyright (c) 2013 Mattias Svala # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 dmpayton # Copyright (c) 2014 dequis # Copyright (c) 2014 Tycho Andersen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import division import math from .base import Layout class Matrix(Layout): """ This layout divides the screen into a matrix of equally sized cells and places one window in each cell. The number of columns is configurable and can also be changed interactively. """ defaults = [ ("border_focus", "#0000ff", "Border colour for the focused window."), ("border_normal", "#000000", "Border colour for un-focused windows."), ("border_width", 1, "Border width."), ("name", "matrix", "Name of this layout."), ("margin", 0, "Margin of the layout"), ] def __init__(self, columns=2, **config): Layout.__init__(self, **config) self.add_defaults(Matrix.defaults) self.current_window = None self.columns = columns self.clients = [] def info(self): d = Layout.info(self) d["rows"] = [ [win.name for win in self.get_row(i)] for i in range(self.get_num_rows()) ] d["current_window"] = self.current_window d["clients"] = [x.name for x in self.clients] return d def clone(self, group): c = Layout.clone(self, group) c.clients = [] return c def get_current_window(self): c, r = self.current_window return self.clients[r * self.columns + c] def get_num_rows(self): return int(math.ceil(len(self.clients) / self.columns)) def get_row(self, row): assert row < self.get_num_rows() return self.clients[ row * self.columns: row * self.columns + self.columns ] def get_column(self, column): assert column < self.columns return [ self.clients[i] for i in range(column, len(self.clients), self.columns) ] def add(self, client): self.clients.append(client) def remove(self, client): if client not in self.clients: return self.clients.remove(client) def focus(self, client): if client not in self.clients: return idx = self.clients.index(client) self.current_window = (idx % self.columns, idx // self.columns) def focus_first(self): if self.clients: return self.clients[0] def focus_last(self): if self.clients: return self.clients[-1] def focus_next(self, window): if not self.clients: return idx = self.clients.index(window) if idx + 1 < len(self.clients): return self.clients[idx + 1] def focus_previous(self, window): if not self.clients: return idx = self.clients.index(window) if idx > 0: return self.clients[idx - 1] def configure(self, client, screen): if client not in self.clients: return idx = self.clients.index(client) column = idx % self.columns row = idx // self.columns column_size = int(math.ceil(len(self.clients) / self.columns)) if client.has_focus: px = self.group.qtile.colorPixel(self.border_focus) else: px = self.group.qtile.colorPixel(self.border_normal) column_width = int(screen.width / float(self.columns)) row_height = int(screen.height / float(column_size)) xoffset = screen.x + column * column_width yoffset = screen.y + row * row_height win_width = column_width - 2 * self.border_width win_height = row_height - 2 * self.border_width client.place( xoffset, yoffset, win_width, win_height, self.border_width, px, margin=self.margin, ) client.unhide() def cmd_next(self): client = self.focus_next(self.get_current_window()) or \ self.focus_first() self.group.focus(client) def cmd_previous(self): client = self.focus_previous(self.get_current_window()) or \ self.focus_last() self.group.focus(client) def cmd_left(self): """Switch to the next window on current row""" column, row = self.current_window self.current_window = ((column - 1) % len(self.get_row(row)), row) self.group.focus(self.get_current_window()) def cmd_right(self): """Switch to the next window on current row""" column, row = self.current_window self.current_window = ((column + 1) % len(self.get_row(row)), row) self.group.focus(self.get_current_window()) def cmd_down(self): """Switch to the next window in current column""" column, row = self.current_window self.current_window = ( column, (row + 1) % len(self.get_column(column)) ) self.group.focus(self.get_current_window()) def cmd_up(self): """Switch to the previous window in current column""" column, row = self.current_window self.current_window = ( column, (row - 1) % len(self.get_column(column)) ) self.group.focus(self.get_current_window()) def cmd_delete(self): """Decrease number of columns""" self.columns -= 1 self.group.layoutAll() def cmd_add(self): """Increase number of columns""" self.columns += 1 self.group.layoutAll() qtile-0.10.7/libqtile/layout/max.py000066400000000000000000000104061305063162100171740ustar00rootroot00000000000000# Copyright (c) 2008, Aldo Cortesi. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from .base import SingleWindow class Max(SingleWindow): """Maximized layout A simple layout that only displays one window at a time, filling the screen. This is suitable for use on laptops and other devices with small screens. Conceptually, the windows are managed as a stack, with commands to switch to next and previous windows in the stack. """ defaults = [("name", "max", "Name of this layout.")] def __init__(self, **config): SingleWindow.__init__(self, **config) self.clients = [] self.add_defaults(Max.defaults) self.current = None def _get_window(self): return self.current def focus(self, client): self.group.layoutAll() self.current = client def focus_first(self): if self.clients: return self.clients[0] def focus_last(self): if self.clients: return self.clients[-1] def focus_next(self, window): if not self.clients: return if window is None: return idx = self.clients.index(window) if idx + 1 < len(self.clients): return self.clients[idx + 1] def focus_previous(self, window): if not self.clients: return if window is None: return idx = self.clients.index(window) if idx > 0: return self.clients[idx - 1] def up(self): client = self.focus_previous(self.current) or self.focus_last() self.group.focus(client, False) def down(self): client = self.focus_next(self.current) or self.focus_first() self.group.focus(client, False) def clone(self, group): c = SingleWindow.clone(self, group) c.clients = [] return c def add(self, client): try: idx = self.clients.index(self._get_window()) except ValueError: self.clients.append(client) else: self.clients.insert(idx + 1, client) def remove(self, client): try: idx = self.clients.index(client) except ValueError: return else: del self.clients[idx] if client is self.current: try: # The previous client must become current # if idx == 0, using self.clients[-1] is indeed correct self.current = self.clients[idx - 1] except IndexError: self.current = None return self.current def configure(self, client, screen): if self.clients and client is self.current: client.place( screen.x, screen.y, screen.width, screen.height, 0, None ) client.unhide() else: client.hide() def info(self): d = SingleWindow.info(self) d["clients"] = [x.name for x in self.clients] return d def cmd_down(self): """Switch down in the window list""" self.down() cmd_next = cmd_down def cmd_up(self): """Switch up in the window list""" self.up() cmd_previous = cmd_up qtile-0.10.7/libqtile/layout/ratiotile.py000066400000000000000000000316431305063162100204110ustar00rootroot00000000000000# -*- coding:utf-8 -*- # Copyright (c) 2011 Florian Mounier # Copyright (c) 2012-2013, 2015 Tycho Andersen # Copyright (c) 2013 Björn Lindström # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 dmpayton # Copyright (c) 2014 dequis # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import division import math from .base import Layout from .. import utils ROWCOL = 1 # do rows at a time left to right top down COLROW = 2 # do cols top to bottom, left to right GOLDEN_RATIO = 1.618 class GridInfo(object): """ Calculates sizes for grids >>> gi = GridInfo(.5, 5, 600, 480) >>> gi.calc() (1, 5, 1) >>> gi.get_sizes() [(0, 0, 120, 480), (120, 0, 120, 480), (240, 0, 120, 480), (360, 0, 120, 480), (480, 0, 120, 480)] >>> gi = GridInfo(6, 5, 600, 480) >>> gi.get_sizes() [(0, 0, 600, 96), (0, 96, 600, 96), (0, 192, 600, 96), (0, 288, 600, 96), (0, 384, 600, 96)] >>> gi = GridInfo(1, 5, 600, 480) >>> gi.get_sizes() [(0, 0, 200, 240), (200, 0, 200, 240), (400, 0, 200, 240), (0, 240, 300, 240), (200, 240, 200, 240)] >>> foo = GridInfo(1.6, 7, 400,370) >>> foo.get_sizes(500,580) """ def __init__(self, ratio, num_windows, width, height): self.ratio = ratio self.num_windows = num_windows self.width = width self.height = height self.num_rows = 0 self.num_cols = 0 def calc(self, num_windows, width, height): """returns (rows, cols, orientation) tuple given input""" best_ratio = None best_rows_cols_orientation = None for rows, cols, orientation in self._possible_grids(num_windows): sample_width = width / cols sample_height = height / rows sample_ratio = sample_width / sample_height diff = abs(sample_ratio - self.ratio) if best_ratio is None or diff < best_ratio: best_ratio = diff best_rows_cols_orientation = (rows, cols, orientation) return best_rows_cols_orientation def _possible_grids(self, num_windows): """ iterates over possible grids given a number of windows """ if num_windows < 2: end = 2 else: end = num_windows // 2 + 1 for rows in range(1, end): cols = int(math.ceil(num_windows / rows)) yield (rows, cols, ROWCOL) if rows != cols: # also want the reverse test yield (cols, rows, COLROW) def get_sizes_advanced(self, total_width, total_height, xoffset=0, yoffset=0): """after every row/column recalculate remaining area""" results = [] width = total_width height = total_height while len(results) < self.num_windows: remaining = self.num_windows - len(results) orien, sizes = self._get_row_or_col( remaining, width, height, xoffset, yoffset ) results.extend(sizes) if orien == ROWCOL: # adjust height/yoffset height -= sizes[-1][-1] yoffset += sizes[-1][-1] else: width -= sizes[-1][-2] xoffset += sizes[-1][-2] return results def _get_row_or_col(self, num_windows, width, height, xoffset, yoffset): """process one row (or col) at a time""" rows, cols, orientation = self.calc(num_windows, width, height) results = [] if orientation == ROWCOL: x = 0 y = 0 for i, col in enumerate(range(cols)): w_width = width // cols w_height = height // rows if i == cols - 1: w_width = width - x results.append((x + xoffset, y + yoffset, w_width, w_height)) x += w_width elif orientation == COLROW: x = 0 y = 0 for i, col in enumerate(range(rows)): w_width = width // cols w_height = height // rows if i == rows - 1: w_height = height - y results.append((x + xoffset, y + yoffset, w_width, w_height)) y += w_height return orientation, results def get_sizes(self, total_width, total_height, xoffset=0, yoffset=0): width = 0 height = 0 results = [] rows, cols, orientation = self.calc( self.num_windows, total_width, total_height ) if orientation == ROWCOL: y = 0 for i, row in enumerate(range(rows)): x = 0 width = total_width // cols for j, col in enumerate(range(cols)): height = total_height // rows if i == rows - 1 and j == 0: # last row remaining = self.num_windows - len(results) width = total_width // remaining elif j == cols - 1 or len(results) + 1 == self.num_windows: # since we are dealing with integers, # make last column (or item) take up remaining space width = total_width - x results.append(( x + xoffset, y + yoffset, width, height )) if len(results) == self.num_windows: return results x += width y += height else: x = 0 for i, col in enumerate(range(cols)): y = 0 height = total_height // rows for j, row in enumerate(range(rows)): width = total_width // cols # down first if i == cols - 1 and j == 0: remaining = self.num_windows - len(results) height = total_height // remaining elif j == rows - 1 or len(results) + 1 == self.num_windows: height = total_height - y results.append(( x + xoffset, # i * width + xoffset, y + yoffset, # j * height + yoffset, width, height )) if len(results) == self.num_windows: return results y += height x += width return results class RatioTile(Layout): """Tries to tile all windows in the width/height ratio passed in""" defaults = [ ("border_focus", "#0000ff", "Border colour for the focused window."), ("border_normal", "#000000", "Border colour for un-focused windows."), ("border_width", 1, "Border width."), ("name", "ratiotile", "Name of this layout."), ("margin", 0, "Margin of the layout"), ("ratio", GOLDEN_RATIO, "Ratio of the tiles"), ("ratio_increment", 0.1, "Amount to increment per ratio increment"), ("fancy", False, "Use a different method to calculate window sizes."), ] def __init__(self, **config): Layout.__init__(self, **config) self.add_defaults(RatioTile.defaults) self.clients = [] self.focused = None self.dirty = True # need to recalculate self.layout_info = [] self.last_size = None self.last_screen = None def clone(self, group): c = Layout.clone(self, group) c.clients = [] return c def focus(self, c): self.focused = c def blur(self): self.focused = None def add(self, w): self.dirty = True self.clients.insert(0, w) def remove(self, w): self.dirty = True if self.focused is w: self.focused = None self.clients.remove(w) if self.clients: # and w is self.focused: self.focused = self.clients[0] return self.focused def configure(self, win, screen): # force recalc if not self.last_screen or self.last_screen != screen: self.last_screen = screen self.dirty = True if self.last_size and not self.dirty: if screen.width != self.last_size[0] or \ screen.height != self.last_size[1]: self.dirty = True if self.dirty: gi = GridInfo( self.ratio, len(self.clients), screen.width, screen.height ) self.last_size = (screen.width, screen.height) if self.fancy: method = gi.get_sizes_advanced else: method = gi.get_sizes self.layout_info = method( screen.width, screen.height, screen.x, screen.y ) self.dirty = False try: idx = self.clients.index(win) except ValueError: win.hide() return x, y, w, h = self.layout_info[idx] if win.has_focus: bc = self.group.qtile.colorPixel(self.border_focus) else: bc = self.group.qtile.colorPixel(self.border_normal) win.place( x, y, w - self.border_width * 2, h - self.border_width * 2, self.border_width, bc, margin=self.margin, ) win.unhide() def info(self): return { 'clients': [x.name for x in self.clients], 'ratio': self.ratio, 'focused': self.focused.name if self.focused else None, 'layout_info': self.layout_info } def shuffleUp(self): if self.clients: utils.shuffleUp(self.clients) self.group.layoutAll() def shuffleDown(self): if self.clients: utils.shuffleDown(self.clients) self.group.layoutAll() def focus_first(self): if self.clients: return self.clients[0] def focus_next(self, win): idx = self.clients.index(win) if len(self.clients) > idx + 1: return self.clients[idx + 1] def focus_last(self): if self.clients: return self.clients[-1] def focus_previous(self, win): idx = self.clients.index(win) if idx > 0: return self.clients[idx - 1] def getNextClient(self): previndex = self.clients.index(self.focused) - 1 if previndex < 0: previndex = len(self.clients) - 1 return self.clients[previndex] def getPreviousClient(self): nextindex = self.clients.index(self.focused) + 1 if nextindex >= len(self.clients): nextindex = 0 return self.clients[nextindex] def next(self): n = self.getPreviousClient() self.group.focus(n) def previous(self): n = self.getNextClient() self.group.focus(n) def shuffle(self, function): if self.clients: function(self.clients) self.group.layoutAll() def cmd_down(self): self.previous() def cmd_up(self): self.next() def cmd_next(self): self.next() def cmd_previous(self): self.previous() def cmd_shuffle_down(self): self.shuffleDown() def cmd_shuffle_up(self): self.shuffleUp() def cmd_decrease_ratio(self): new_ratio = self.ratio - self.ratio_increment if new_ratio < 0: return self.ratio = new_ratio self.group.layoutAll() def cmd_increase_ratio(self): self.ratio += self.ratio_increment self.group.layoutAll() def cmd_info(self): return self.info() if __name__ == '__main__': import doctest doctest.testmod() qtile-0.10.7/libqtile/layout/slice.py000066400000000000000000000114601305063162100175070ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2011 Florian Mounier # Copyright (c) 2012, 2015 Tycho Andersen # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sean Vig # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """ Slice layout. Serves as example of delegating layouts (or sublayouts) """ from .base import Layout, SingleWindow, Delegate from .max import Max class Single(SingleWindow): """Layout with single window Just like Max but asserts that window is the one """ def __init__(self): SingleWindow.__init__(self) self.window = None self.focused = False def add(self, window): assert self.window is None self.window = window def remove(self, window): assert self.window is window self.window = None def _get_window(self): return self.window def empty(self): """Is the layout empty Returns True if the layout empty (and is willing to accept windows) """ return self.window is None def focus_first(self): self.focused = True return self.window def focus_last(self): self.focused = True return self.window def focus_next(self, window): if self.focused: self.focused = False return None return self.window def focus_previous(self, window): if self.focused: self.focused = False return None return self.window def cmd_next(self): pass def cmd_previous(self): pass class Slice(Delegate): """Slice layout This layout cuts piece of screen and places a single window on that piece, and delegates other window placement to other layout """ defaults = [ ("width", 256, "Slice width"), ("side", "left", "Side of the slice (left, right, top, bottom)"), ("name", "max", "Name of this layout."), ("wname", None, "WM_NAME to match"), ("wmclass", None, "WM_CLASS to match"), ("role", None, "WM_WINDOW_ROLE to match"), ("fallback", Max(), "Fallback layout"), ] def __init__(self, **config): Delegate.__init__(self, **config) self.add_defaults(Slice.defaults) self.match = { 'wname': self.wname, 'wmclass': self.wmclass, 'role': self.role, } self._slice = Single() def clone(self, group): res = Layout.clone(self, group) res._slice = self._slice.clone(group) res.fallback = self.fallback.clone(group) res._window = None return res def layout(self, windows, screen): if self.side == 'left': win, sub = screen.hsplit(self.width) elif self.side == 'right': sub, win = screen.hsplit(screen.width - self.width) elif self.side == 'top': win, sub = screen.vsplit(self.width) elif self.side == 'bottom': sub, win = screen.vsplit(screen.height - self.width) else: raise NotImplementedError(self.side) self.delegate_layout( windows, { self._slice: win, self.fallback: sub, } ) def configure(self, win, screen): raise NotImplementedError("Should not be called") def _get_layouts(self): return (self._slice, self.fallback) def _get_active_layout(self): return self.fallback # always def add(self, win): if self._slice.empty() and win.match(**self.match): self._slice.add(win) self.layouts[win] = self._slice else: self.fallback.add(win) self.layouts[win] = self.fallback def cmd_next(self): self.fallback.cmd_next() def cmd_previous(self): self.fallback.cmd_previous() qtile-0.10.7/libqtile/layout/stack.py000066400000000000000000000301661305063162100175210ustar00rootroot00000000000000# Copyright (c) 2008, Aldo Cortesi. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import division from .base import Layout from .. import utils class _WinStack(object): def __init__(self, autosplit=False): self.split = autosplit self._current = 0 self.lst = [] @property def current(self): return self._current @current.setter def current(self, x): if len(self): self._current = abs(x % len(self)) else: self._current = 0 @property def cw(self): if not self.lst: return None return self.lst[self.current] def toggleSplit(self): self.split = False if self.split else True def join(self, ws): # FIXME: This buggers up window order - # windows should be injected BEFORE # the current offset. self.lst.extend(ws.lst) def focus(self, client): self.current = self.lst.index(client) def focus_first(self): if self.split: return self[0] else: return self.cw def focus_next(self, win): if self.split: idx = self.index(win) if idx + 1 < len(self): return self[idx + 1] def focus_last(self): if self.split: return self[-1] else: return self.cw def focus_previous(self, win): if self.split: idx = self.index(win) if idx > 0: return self[idx - 1] def add(self, client): self.lst.insert(self.current, client) def remove(self, client): if client not in self.lst: return idx = self.lst.index(client) del self.lst[idx] if idx > self.current: self.current -= 1 else: # This apparently nonsensical assignment caps the value using the # property definition. self.current = self.current def index(self, client): return self.lst.index(client) def __len__(self): return len(self.lst) def __getitem__(self, i): return self.lst[i] def __contains__(self, client): return client in self.lst def __str__(self): return "_WinStack: %s, %s" % ( self.current, str([i.name for i in self]) ) def info(self): return dict( clients=[x.name for x in self], split=self.split, current=self.current, ) class Stack(Layout): """A layout composed of stacks of windows The stack layout divides the screen horizontally into a set of stacks. Commands allow you to switch between stacks, to next and previous windows within a stack, and to split a stack to show all windows in the stack, or unsplit it to show only the current window. At the moment, this is the most mature and flexible layout in Qtile. """ defaults = [ ("border_focus", "#0000ff", "Border colour for the focused window."), ("border_normal", "#000000", "Border colour for un-focused windows."), ("border_width", 1, "Border width."), ("name", "stack", "Name of this layout."), ("autosplit", False, "Auto split all new stacks."), ("num_stacks", 2, "Number of stacks."), ("fair", False, "Add new windows to the stacks in a round robin way."), ("margin", 0, "Margin of the layout"), ] def __init__(self, **config): Layout.__init__(self, **config) self.add_defaults(Stack.defaults) self.stacks = [_WinStack(autosplit=self.autosplit) for i in range(self.num_stacks)] @property def currentStack(self): return self.stacks[self.currentStackOffset] @property def currentStackOffset(self): for i, s in enumerate(self.stacks): if self.group.currentWindow in s: return i return 0 @property def clients(self): client_list = [] for stack in self.stacks: client_list.extend(list(stack)) return client_list def clone(self, group): c = Layout.clone(self, group) # These are mutable c.stacks = [_WinStack(autosplit=self.autosplit) for i in self.stacks] return c def _findNext(self, lst, offset): for i in lst[offset + 1:]: if i: return i else: for i in lst[:offset]: if i: return i def deleteCurrentStack(self): if len(self.stacks) > 1: off = self.currentStackOffset or 0 s = self.stacks[off] self.stacks.remove(s) off = min(off, len(self.stacks) - 1) self.stacks[off].join(s) if self.stacks[off]: self.group.focus( self.stacks[off].cw, False ) def nextStack(self): n = self._findNext( self.stacks, self.currentStackOffset ) if n: self.group.focus(n.cw, True) def previousStack(self): n = self._findNext( list(reversed(self.stacks)), len(self.stacks) - self.currentStackOffset - 1 ) if n: self.group.focus(n.cw, True) def focus(self, client): for i in self.stacks: if client in i: i.focus(client) def focus_first(self): for i in self.stacks: if i: return i.focus_first() def focus_last(self): for i in reversed(self.stacks): if i: return i.focus_last() def focus_next(self, client): iterator = iter(self.stacks) for i in iterator: if client in i: next = i.focus_next(client) if next: return next break else: return for i in iterator: if i: return i.focus_first() def focus_previous(self, client): iterator = iter(reversed(self.stacks)) for i in iterator: if client in i: next = i.focus_previous(client) if next: return next break else: return for i in iterator: if i: return i.focus_last() def add(self, client): for i in self.stacks: if not i: i.add(client) return if self.fair: target = min(self.stacks, key=len) target.add(client) else: self.currentStack.add(client) def remove(self, client): currentOffset = self.currentStackOffset for i in self.stacks: if client in i: i.remove(client) break if self.stacks[currentOffset].cw: return self.stacks[currentOffset].cw else: n = self._findNext( list(reversed(self.stacks)), len(self.stacks) - currentOffset - 1 ) if n: return n.cw def configure(self, client, screen): for i, s in enumerate(self.stacks): if client in s: break else: client.hide() return if client.has_focus: px = self.group.qtile.colorPixel(self.border_focus) else: px = self.group.qtile.colorPixel(self.border_normal) columnWidth = int(screen.width / len(self.stacks)) xoffset = screen.x + i * columnWidth winWidth = columnWidth - 2 * self.border_width if s.split: columnHeight = int(screen.height / len(s)) winHeight = columnHeight - 2 * self.border_width yoffset = screen.y + s.index(client) * columnHeight client.place( xoffset, yoffset, winWidth, winHeight, self.border_width, px, margin=self.margin, ) client.unhide() else: if client == s.cw: client.place( xoffset, screen.y, winWidth, screen.height - 2 * self.border_width, self.border_width, px, margin=self.margin, ) client.unhide() else: client.hide() def info(self): d = Layout.info(self) d["stacks"] = [i.info() for i in self.stacks] d["current_stack"] = self.currentStackOffset d["clients"] = [c.name for c in self.clients] return d def cmd_toggle_split(self): """Toggle vertical split on the current stack""" self.currentStack.toggleSplit() self.group.layoutAll() def cmd_down(self): """Switch to the next window in this stack""" self.currentStack.current -= 1 self.group.focus(self.currentStack.cw, False) def cmd_up(self): """Switch to the previous window in this stack""" self.currentStack.current += 1 self.group.focus(self.currentStack.cw, False) def cmd_shuffle_up(self): """Shuffle the order of this stack up""" utils.shuffleUp(self.currentStack.lst) self.currentStack.current += 1 self.group.layoutAll() def cmd_shuffle_down(self): """Shuffle the order of this stack down""" utils.shuffleDown(self.currentStack.lst) self.currentStack.current -= 1 self.group.layoutAll() def cmd_delete(self): """Delete the current stack from the layout""" self.deleteCurrentStack() def cmd_add(self): """Add another stack to the layout""" newstack = _WinStack(autosplit=self.autosplit) if self.autosplit: newstack.split = True self.stacks.append(newstack) self.group.layoutAll() def cmd_rotate(self): """Rotate order of the stacks""" utils.shuffleUp(self.stacks) self.group.layoutAll() def cmd_next(self): """Focus next stack""" return self.nextStack() def cmd_previous(self): """Focus previous stack""" return self.previousStack() def cmd_client_to_next(self): """Send the current client to the next stack""" return self.cmd_client_to_stack(self.currentStackOffset + 1) def cmd_client_to_previous(self): """Send the current client to the previous stack""" return self.cmd_client_to_stack(self.currentStackOffset - 1) def cmd_client_to_stack(self, n): """ Send the current client to stack n, where n is an integer offset. If is too large or less than 0, it is wrapped modulo the number of stacks. """ if not self.currentStack: return next = n % len(self.stacks) win = self.currentStack.cw self.currentStack.remove(win) self.stacks[next].add(win) self.stacks[next].focus(win) self.group.layoutAll() def cmd_info(self): return self.info() qtile-0.10.7/libqtile/layout/tile.py000066400000000000000000000171711305063162100173520ustar00rootroot00000000000000# Copyright (c) 2010 Aldo Cortesi # Copyright (c) 2010-2011 Paul Colomiets # Copyright (c) 2011 Mounier Florian # Copyright (c) 2011 Tzbob # Copyright (c) 2012 roger # Copyright (c) 2012-2014 Tycho Andersen # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 dmpayton # Copyright (c) 2014 dequis # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import division from .base import Layout from .. import utils class Tile(Layout): defaults = [ ("border_focus", "#0000ff", "Border colour for the focused window."), ("border_normal", "#000000", "Border colour for un-focused windows."), ("border_width", 1, "Border width."), ("name", "tile", "Name of this layout."), ("margin", 0, "Margin of the layout"), ] def __init__(self, ratio=0.618, masterWindows=1, expand=True, ratio_increment=0.05, add_on_top=True, shift_windows=False, master_match=None, **config): Layout.__init__(self, **config) self.add_defaults(Tile.defaults) self.clients = [] self.ratio = ratio self.master = masterWindows self.focused = None self.expand = expand self.ratio_increment = ratio_increment self.add_on_top = add_on_top self.shift_windows = shift_windows self.master_match = master_match @property def master_windows(self): return self.clients[:self.master] @property def slave_windows(self): return self.clients[self.master:] def up(self): if self.shift_windows: self.shift_up() else: self.shuffle(utils.shuffleUp) def down(self): if self.shift_windows: self.shift_down() else: self.shuffle(utils.shuffleDown) def shift_up(self): if self.clients: currentindex = self.clients.index(self.focused) nextindex = (currentindex + 1) % len(self.clients) self.shift(currentindex, nextindex) def shift_down(self): if self.clients: currentindex = self.clients.index(self.focused) previndex = (currentindex - 1) % len(self.clients) self.shift(currentindex, previndex) def focus_first(self): if self.clients: return self.clients[0] def focus_next(self, client): if client not in self.clients: return idx = self.clients.index(client) if len(self.clients) > idx + 1: return self.clients[idx + 1] def focus_last(self): if self.clients: return self.clients[-1] def focus_previous(self, client): if client not in self.clients: return idx = self.clients.index(client) if idx > 0: return self.clients[idx - 1] def shuffle(self, function): if self.clients: function(self.clients) self.group.layoutAll(True) def resetMaster(self, match=None): if not match and self.master_match: match = self.master_match else: return if self.clients: masters = [c for c in self.clients if match.compare(c)] self.clients = masters + [ c for c in self.clients if c not in masters ] def shift(self, idx1, idx2): if self.clients: self.clients[idx1], self.clients[idx2] = \ self.clients[idx2], self.clients[idx1] self.group.layoutAll(True) def clone(self, group): c = Layout.clone(self, group) c.clients = [] return c def focus(self, client): self.focused = client def blur(self): self.focused = None def add(self, client): index = 0 if not self.add_on_top and self.clients and self.focused: index = self.clients.index(self.focused) self.clients.insert(index, client) self.resetMaster() def remove(self, client): if client not in self.clients: return if self.focused is client: self.focused = None self.clients.remove(client) if self.clients and client is self.focused: self.focused = self.clients[0] return self.focused def configure(self, client, screen): screenWidth = screen.width screenHeight = screen.height x = 0 y = 0 w = 0 h = 0 borderWidth = self.border_width if self.clients and client in self.clients: pos = self.clients.index(client) if client in self.master_windows: w = int(screenWidth * self.ratio) \ if len(self.slave_windows) or not self.expand \ else screenWidth h = screenHeight // self.master x = screen.x y = screen.y + pos * h else: w = screenWidth - int(screenWidth * self.ratio) h = screenHeight // (len(self.slave_windows)) x = screen.x + int(screenWidth * self.ratio) y = screen.y + self.clients[self.master:].index(client) * h if client.has_focus: bc = self.group.qtile.colorPixel(self.border_focus) else: bc = self.group.qtile.colorPixel(self.border_normal) client.place( x, y, w - borderWidth * 2, h - borderWidth * 2, borderWidth, bc, margin=self.margin, ) client.unhide() else: client.hide() def info(self): return dict( clients=[c.name for c in self.clients], master=[c.name for c in self.master_windows], slave=[c.name for c in self.slave_windows], ) def cmd_down(self): self.down() def cmd_up(self): self.up() def cmd_next(self): client = self.focus_next(self.focused) or self.focus_first() self.group.focus(client) def cmd_previous(self): client = self.focus_previous(self.focused) or self.focus_last() self.group.focus(client) def cmd_decrease_ratio(self): self.ratio -= self.ratio_increment self.group.layoutAll() def cmd_increase_ratio(self): self.ratio += self.ratio_increment self.group.layoutAll() def cmd_decrease_nmaster(self): self.master -= 1 if self.master <= 0: self.master = 1 self.group.layoutAll() def cmd_increase_nmaster(self): self.master += 1 self.group.layoutAll() qtile-0.10.7/libqtile/layout/tree.py000066400000000000000000000526021305063162100173520ustar00rootroot00000000000000# -*- coding:utf-8 -*- # Copyright (c) 2011 Mounier Florian # Copyright (c) 2011 Paul Colomiets # Copyright (c) 2012 roger # Copyright (c) 2012-2014 Tycho Andersen # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2013 Arnas Udovicius # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Nathan Hoad # Copyright (c) 2014 dequis # Copyright (c) 2014 Thomas Sarboni # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from .base import Layout from .. import drawer, hook, window to_superscript = dict(zip(map(ord, u'0123456789'), map(ord, u'⁰¹²³⁴⁵⁶⁷⁸⁹'))) class TreeNode(object): def __init__(self): self.children = [] self.parent = None self.expanded = True self._children_top = None self._children_bot = None def add(self, node, hint=None): """Add a node below this node The `hint` is a node to place the new node after in this nodes children. """ node.parent = self if hint is not None: try: idx = self.children.index(hint) except ValueError: pass else: self.children.insert(idx + 1, node) return self.children.append(node) def draw(self, layout, top, level=0): """Draw the node and its children to a layout Draws this node to the given layout (presumably a TreeTab), starting from a y-offset of `top` and at the given level. """ self._children_top = top if self.expanded: for i in self.children: top = i.draw(layout, top, level) self._children_bot = top return top def button_press(self, x, y): """Returns self or sibling which got the click""" # if we store the locations of each child, it would be possible to do # this without having to traverse the tree... if not (self._children_top <= y < self._children_bot): return for i in self.children: res = i.button_press(x, y) if res is not None: return res def add_superscript(self, title): """Prepend superscript denoting the number of hidden children""" if not self.expanded and self.children: return u"{:d}".format( len(self.children) ).translate(to_superscript).encode('utf-8') + title return title def get_first_window(self): """Find the first Window under this node Returns self if this is a `Window`, otherwise finds first `Window` by depth-first search """ if isinstance(self, Window): return self for i in self.children: node = i.get_first_window() if node: return node def get_last_window(self): """Find the last Window under this node Finds last `Window` by depth-first search, otherwise returns self if this is a `Window`. """ for i in reversed(self.children): node = i.get_last_window() if node: return node if isinstance(self, Window): return self def get_next_window(self): if self.children and self.expanded: return self.children[0] node = self while not isinstance(node, Root): parent = node.parent idx = parent.children.index(node) for i in range(idx + 1, len(parent.children)): res = parent.children[i].get_first_window() if res: return res node = parent def get_prev_window(self): node = self while not isinstance(node, Root): parent = node.parent idx = parent.children.index(node) if idx == 0 and isinstance(parent, Window): return parent for i in range(idx - 1, -1, -1): res = parent.children[i].get_last_window() if res: return res node = parent class Root(TreeNode): def __init__(self, sections, default_section=None): super(Root, self).__init__() self.sections = {} for section in sections: self.add_section(section) if default_section is None: self.def_section = self.children[0] else: self.def_section = self.sections[default_section] def add(self, win, hint=None): """Add a new window Adds a new `Window` to the tree. The location of the new node is controlled by looking: * `hint` kwarg - place the node next to this node * win.tree_section - place the window in the given section, by name * default section - fallback to default section (first section, if not otherwise set) """ parent = None if hint is not None: parent = hint.parent if parent is None: sect = getattr(win, 'tree_section', None) if sect is not None: parent = self.sections.get(sect) if parent is None: parent = self.def_section node = Window(win) parent.add(node, hint=hint) return node def add_section(self, name): """Add a new Section with the given name""" if name in self.sections: raise ValueError("Duplicate section name") node = Section(name) node.parent = self self.sections[name] = node self.children.append(node) def del_section(self, name): """Remove the Section with the given name""" if name not in self.sections: raise ValueError("Section name not found") if len(self.children) == 1: raise ValueError("Can't delete last section") sec = self.sections[name] # move the children of the deleted section to the previous section # if delecting the first section, add children to second section idx = min(self.children.index(sec), 1) next_sec = self.children[idx - 1] # delete old section, reparent children to next section del self.children[idx] next_sec.children.extend(sec.children) for i in sec.children: i.parent = next_sec class Section(TreeNode): def __init__(self, title): super(Section, self).__init__() self.title = title def draw(self, layout, top, level=0): del layout._layout.width # no centering # draw a horizontal line above the section layout._drawer.draw_hbar( layout.section_fg, 0, layout.panel_width, top, linewidth=1 ) # draw the section title layout._layout.font_size = layout.section_fontsize layout._layout.text = self.add_superscript(self.title) layout._layout.colour = layout.section_fg layout._layout.draw( x=layout.section_left, y=top + layout.section_top ) top += layout._layout.height + \ layout.section_top + \ layout.section_padding # run the TreeNode draw to draw children (if expanded) top = super(Section, self).draw(layout, top, level) return top + layout.section_bottom class Window(TreeNode): def __init__(self, win): super(Window, self).__init__() self.window = win self._title_top = None def draw(self, layout, top, level=0): self._title_top = top # setup parameters for drawing self left = layout.padding_left + level * layout.level_shift layout._layout.font_size = layout.fontsize layout._layout.text = self.add_superscript(self.window.name) if self.window is layout._focused: fg = layout.active_fg bg = layout.active_bg else: fg = layout.inactive_fg bg = layout.inactive_bg layout._layout.colour = fg layout._layout.width = layout.panel_width - left # get a text frame from the above framed = layout._layout.framed( layout.border_width, bg, layout.padding_x, layout.padding_y ) # draw the text frame at the given point framed.draw_fill(left, top) top += framed.height + layout.vspace + layout.border_width # run the TreeNode draw to draw children (if expanded) return super(Window, self).draw(layout, top, level + 1) def button_press(self, x, y): """Returns self if clicked on title else returns sibling""" if self._title_top <= y < self._children_top: return self return super(Window, self).button_press(x, y) def remove(self): """Removes this Window If this window has children, the first child takes the place of this window, and any remaining children are reparented to that node """ if self.children: head = self.children[0] # add the first child to our parent, next to ourselves self.parent.add(head, hint=self) # move remaining children to be under the new head for i in self.children[1:]: head.add(i) self.parent.children.remove(self) del self.children class TreeTab(Layout): """Tree Tab Layout This layout works just like Max but displays tree of the windows at the left border of the screen, which allows you to overview all opened windows. It's designed to work with ``uzbl-browser`` but works with other windows too. """ defaults = [ ("bg_color", "000000", "Background color of tabs"), ("active_bg", "000080", "Background color of active tab"), ("active_fg", "ffffff", "Foreground color of active tab"), ("inactive_bg", "606060", "Background color of inactive tab"), ("inactive_fg", "ffffff", "Foreground color of inactive tab"), ("margin_left", 6, "Left margin of tab panel"), ("margin_y", 6, "Vertical margin of tab panel"), ("padding_left", 6, "Left padding for tabs"), ("padding_x", 6, "Left padding for tab label"), ("padding_y", 2, "Top padding for tab label"), ("border_width", 2, "Width of the border"), ("vspace", 2, "Space between tabs"), ("level_shift", 8, "Shift for children tabs"), ("font", "Arial", "Font"), ("fontsize", 14, "Font pixel size."), ("fontshadow", None, "font shadow color, default is None (no shadow)"), ("section_fontsize", 11, "Font pixel size of section label"), ("section_fg", "ffffff", "Color of section label"), ("section_top", 4, "Top margin of section label"), ("section_bottom", 6, "Bottom margin of section"), ("section_padding", 4, "Bottom of margin section label"), ("section_left", 4, "Left margin of section label"), ("panel_width", 150, "Width of the left panel"), ("sections", ['Default'], "Foreground color of inactive tab"), ("name", "treetab", "Name of this layout."), ("previous_on_rm", False, "Focus previous window on close instead of first."), ] def __init__(self, **config): Layout.__init__(self, **config) self.add_defaults(TreeTab.defaults) self._focused = None self._panel = None self._drawer = None self._layout = None self._tree = Root(self.sections) self._nodes = {} def clone(self, group): c = Layout.clone(self, group) c._focused = None c._panel = None c._tree = Root(self.sections) return c def _get_window(self): return self._focused def focus(self, win): self._focused = win def focus_first(self): win = self._tree.get_first_window() if win: return win.window def focus_last(self): win = self._tree.get_last_window() if win: return win.window def focus_next(self, client): win = self._nodes[client].get_next_window() if win: return win.window def focus_previous(self, client): win = self._nodes[client].get_prev_window() if win: return win.window def blur(self): # Does not clear current window, will change if new one # will be focused. This works better when floating window # will be next focused one pass def add(self, win): if self._focused: node = self._tree.add(win, hint=self._nodes[self._focused]) else: node = self._tree.add(win) self._nodes[win] = node def remove(self, win): if win not in self._nodes: return if self.previous_on_rm: self._focused = self.focus_previous(win) else: self._focused = self.focus_first() if self._focused is win: self._focused = None self._nodes[win].remove() del self._nodes[win] self.draw_panel() def _create_panel(self): self._panel = window.Internal.create( self.group.qtile, 0, 0, self.panel_width, 100 ) self._create_drawer() self._panel.handle_Expose = self._panel_Expose self._panel.handle_ButtonPress = self._panel_ButtonPress self.group.qtile.windowMap[self._panel.window.wid] = self._panel hook.subscribe.window_name_change(self.draw_panel) hook.subscribe.focus_change(self.draw_panel) def _panel_Expose(self, e): self.draw_panel() def draw_panel(self): if not self._panel: return self._drawer.clear(self.bg_color) self._tree.draw(self, 0) self._drawer.draw(offsetx=0, width=self.panel_width) def _panel_ButtonPress(self, event): node = self._tree.button_press(event.event_x, event.event_y) if node: self.group.focus(node.window, False) def configure(self, client, screen): if self._nodes and client is self._focused: client.place( screen.x, screen.y, screen.width, screen.height, 0, None ) client.unhide() else: client.hide() def finalize(self): Layout.finalize(self) if self._drawer is not None: self._drawer.finalize() def info(self): d = Layout.info(self) d["clients"] = [x.name for x in self._nodes] d["sections"] = [x.title for x in self._tree.children] return d def show(self, screen): if not self._panel: self._create_panel() panel, body = screen.hsplit(self.panel_width) self._resize_panel(panel) self._panel.unhide() def hide(self): if self._panel: self._panel.hide() def cmd_down(self): """Switch down in the window list""" win = None if self._focused: win = self._nodes[self._focused].get_next_window() if not win: win = self._tree.get_first_window() if win: self.group.focus(win.window, False) self._focused = win.window if win else None cmd_next = cmd_down def cmd_up(self): """Switch up in the window list""" win = None if self._focused: win = self._nodes[self._focused].get_prev_window() if not win: win = self._tree.get_last_window() if win: self.group.focus(win.window, False) self._focused = win.window if win else None cmd_previous = cmd_up def cmd_move_up(self): win = self._focused if not win: return node = self._nodes[win] p = node.parent.children idx = p.index(node) if idx > 0: p[idx] = p[idx - 1] p[idx - 1] = node self.draw_panel() def cmd_move_down(self): win = self._focused if not win: return node = self._nodes[win] p = node.parent.children idx = p.index(node) if idx < len(p) - 1: p[idx] = p[idx + 1] p[idx + 1] = node self.draw_panel() def cmd_move_left(self): win = self._focused if not win: return node = self._nodes[win] if not isinstance(node.parent, Section): node.parent.children.remove(node) node.parent.parent.add(node) self.draw_panel() def cmd_add_section(self, name): """Add named section to tree""" self._tree.add_section(name) self.draw_panel() def cmd_del_section(self, name): """Add named section to tree""" self._tree.del_section(name) self.draw_panel() def cmd_section_up(self): win = self._focused if not win: return node = self._nodes[win] snode = node while not isinstance(snode, Section): snode = snode.parent idx = snode.parent.children.index(snode) if idx > 0: node.parent.children.remove(node) snode.parent.children[idx - 1].add(node) self.draw_panel() def cmd_section_down(self): win = self._focused if not win: return node = self._nodes[win] snode = node while not isinstance(snode, Section): snode = snode.parent idx = snode.parent.children.index(snode) if idx < len(snode.parent.children) - 1: node.parent.children.remove(node) snode.parent.children[idx + 1].add(node) self.draw_panel() def cmd_sort_windows(self, sorter, create_sections=True): """Sorts window to sections using sorter function Parameters ========== sorter : function with single arg returning string returns name of the section where window should be create_sections : if this parameter is True (default), if sorter returns unknown section name it will be created dynamically """ for sec in self._tree.children: for win in sec.children[:]: nname = sorter(win.window) if nname is None or nname == sec.title: continue try: nsec = self._tree.sections[nname] except KeyError: if create_sections: self._tree.add_section(nname) nsec = self._tree.sections[nname] else: continue sec.children.remove(win) nsec.children.append(win) win.parent = nsec self.draw_panel() def cmd_move_right(self): win = self._focused if not win: return node = self._nodes[win] idx = node.parent.children.index(node) if idx > 0: node.parent.children.remove(node) node.parent.children[idx - 1].add(node) self.draw_panel() def cmd_expand_branch(self): if not self._focused: return self._nodes[self._focused].expanded = True self.draw_panel() def cmd_collapse_branch(self): if not self._focused: return self._nodes[self._focused].expanded = False self.draw_panel() def cmd_increase_ratio(self): self.panel_width += 10 self.group.layoutAll() def cmd_decrease_ratio(self): self.panel_width -= 10 self.group.layoutAll() def _create_drawer(self): if self._drawer is None: self._drawer = drawer.Drawer( self.group.qtile, self._panel.window.wid, self.panel_width, self.group.screen.dheight ) self._drawer.clear(self.bg_color) self._layout = self._drawer.textlayout( "", "ffffff", self.font, self.fontsize, self.fontshadow, wrap=False ) def layout(self, windows, screen): panel, body = screen.hsplit(self.panel_width) self._resize_panel(panel) Layout.layout(self, windows, body) def _resize_panel(self, rect): if self._panel: self._panel.place( rect.x, rect.y, rect.width, rect.height, 0, None ) self._create_drawer() self.draw_panel() qtile-0.10.7/libqtile/layout/verticaltile.py000066400000000000000000000243551305063162100211060ustar00rootroot00000000000000# Copyright (c) 2014, Florian Scherf . All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import division from .base import Layout class VerticalTile(Layout): """Tiling layout that works nice on vertically mounted monitors The available height gets divided by the number of panes, if no pane is maximized. If one pane has been maximized, the available height gets split in master- and secondary area. The maximized pane (master pane) gets the full height of the master area and the other panes (secondary panes) share the remaining space. The master area (at default 75%) can grow and shrink via keybindings. :: ----------------- ----------------- --- | | | | | | 1 | <-- Panes | | | | | | | | | |---------------| | | | | | | | | | | | 2 | <-----+ | 1 | | Master Area | | | | | | |---------------| | | | | | | | | | | | 3 | <-----+ | | | | | | | | | |---------------| | |---------------| --- | | | | 2 | | | 4 | <-----+ |---------------| | Secondary Area | | | 3 | | ----------------- ----------------- --- Normal behavior. No One maximized pane in the master area maximized pane. No and two secondary panes in the specific areas. secondary area. :: ----------------------------------- In some cases VerticalTile can be | | useful on horizontal mounted | 1 | monitors two. | | For example if you want to have a |---------------------------------| webbrowser and a shell below it. | | | 2 | | | ----------------------------------- Suggested keybindings: :: Key([modkey], 'j', lazy.layout.down()), Key([modkey], 'k', lazy.layout.up()), Key([modkey], 'Tab', lazy.layout.next()), Key([modkey, 'shift'], 'Tab', lazy.layout.next()), Key([modkey, 'shift'], 'j', lazy.layout.shuffle_down()), Key([modkey, 'shift'], 'k', lazy.layout.shuffle_up()), Key([modkey], 'm', lazy.layout.maximize()), Key([modkey], 'n', lazy.layout.normalize()), """ defaults = [ ('border_focus', '#FF0000', 'Border color for the focused window.'), ('border_normal', '#FFFFFF', 'Border color for un-focused windows.'), ('border_width', 1, 'Border width.'), ('margin', 0, 'Border margin.'), ('name', 'VerticalTile', 'Name of this layout.'), ] ratio = 0.75 steps = 0.05 def __init__(self, **config): Layout.__init__(self, **config) self.add_defaults(self.defaults) self.clients = [] self.focused = None self.maximized = None def add(self, window): if self.clients and self.focused: index = self.clients.index(self.focused) self.clients.insert(index + 1, window) else: self.clients.append(window) self.focus(window) def remove(self, window): if window not in self.clients: return index = self.clients.index(window) self.clients.remove(window) if not self.clients: self.focused = None self.maximized = None return if self.maximized is window: self.maximized = None if index == len(self.clients): index -= 1 self.focus(self.clients[index]) return self.focused def clone(self, group): c = Layout.clone(self, group) c.clients = [] c.focused = None return c def configure(self, window, screen): if self.clients and window in self.clients: n = len(self.clients) index = self.clients.index(window) # border if n > 1: border_width = self.border_width else: border_width = 0 if window.has_focus: border_color = self.group.qtile.colorPixel(self.border_focus) else: border_color = self.group.qtile.colorPixel(self.border_normal) # width if n > 1: width = screen.width - self.border_width * 2 else: width = screen.width # height if n > 1: main_area_height = int(screen.height * self.ratio) sec_area_height = screen.height - main_area_height main_pane_height = main_area_height - border_width * 2 sec_pane_height = sec_area_height // (n - 1) - border_width * 2 normal_pane_height = (screen.height // n) - (border_width * 2) if self.maximized: if window is self.maximized: height = main_pane_height else: height = sec_pane_height else: height = normal_pane_height else: height = screen.height # y y = screen.y if n > 1: if self.maximized: y += (index * sec_pane_height) + (border_width * 2 * index) else: y += (index * normal_pane_height) +\ (border_width * 2 * index) if self.maximized and window is not self.maximized: if index > self.clients.index(self.maximized): y = y - sec_pane_height + main_pane_height window.place(screen.x, y, width, height, border_width, border_color, margin=self.margin) window.unhide() else: window.hide() def blur(self): self.focused = None def focus(self, window): self.focused = window def focus_first(self): try: return self.clients[0] except IndexError: pass def focus_last(self): try: return self.clients[-1] except IndexError: pass def focus_next(self, window): if not self.clients: return try: index = self.clients.index(window) return self.clients[index + 1] except IndexError: pass def focus_previous(self, window): if not self.clients: return try: index = self.clients.index(window) return self.clients[index - 1] except IndexError: pass def grow(self): if self.ratio + self.steps < 1: self.ratio += self.steps self.group.layoutAll() def shrink(self): if self.ratio - self.steps > 0: self.ratio -= self.steps self.group.layoutAll() def cmd_next(self): self.focus_next(self.focused) self.group.focus(self.focused) def cmd_previous(self): self.focus_previous(self.focused) self.group.focus(self.focused) def cmd_down(self): self.focus_next(self.focused) self.group.focus(self.focused) def cmd_up(self): self.focus_previous(self.focused) self.group.focus(self.focused) def cmd_shuffle_up(self): index = self.clients.index(self.focused) try: self.clients[index], self.clients[index - 1] =\ self.clients[index - 1], self.clients[index] except IndexError: self.clients[index], self.clients[-1] =\ self.clients[-1], self.clients[index] self.group.layoutAll() def cmd_shuffle_down(self): index = self.clients.index(self.focused) try: self.clients[index], self.clients[index + 1] =\ self.clients[index + 1], self.clients[index] except IndexError: self.clients[index], self.clients[0] =\ self.clients[0], self.clients[index] self.group.layoutAll() def cmd_maximize(self): if self.clients: self.maximized = self.focused self.group.layoutAll() def cmd_normalize(self): self.maximized = None self.group.layoutAll() def cmd_grow(self): if not self.maximized: return if self.focused is self.maximized: self.grow() else: self.shrink() def cmd_shrink(self): if not self.maximized: return if self.focused is self.maximized: self.shrink() else: self.grow() qtile-0.10.7/libqtile/layout/wmii.py000066400000000000000000000365651305063162100173720ustar00rootroot00000000000000# Copyright (c) 2013 Mattias Svala # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 dmpayton # Copyright (c) 2014 dequis # Copyright (c) 2014 Tycho Andersen # Copyright (c) 2015 Serge Hallyn # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import division from .base import Layout # We have an array of columns. Each columns is a dict containing # width (in percent), rows (an array of rows), and mode, which is # either 'stack' or 'split' # # Each row is an array of clients class Wmii(Layout): """This layout emulates wmii layouts The screen it split into columns, always starting with one. A new window is created in the active window's column. Windows can be shifted left and right. If there is no column when shifting, a new one is created. Each column can be stacked or divided (equally split). This layout implements something akin to wmii's semantics. Each group starts with one column. The first window takes up the whole screen. Next window splits the column in half. Windows can be moved to the column to the left or right. If there is no column in the direction being moved into, a new column is created. Each column can be either stacked (each window takes up the whole vertical real estate) or split (the windows are split equally vertically in the column) Columns can be grown horizontally (cmd_grow_left/right). My config.py has the following added:: Key( [mod, "shift", "control"], "l", lazy.layout.grow_right() ), Key( [mod, "shift"], "l", lazy.layout.shuffle_right() ), Key( [mod, "shift", "control"], "h", lazy.layout.grow_left() ), Key( [mod, "shift"], "h", lazy.layout.shuffle_left() ), Key( [mod], "s", lazy.layout.toggle_split() ), """ defaults = [ ("border_focus", "#881111", "Border colour for the focused window."), ("border_normal", "#220000", "Border colour for un-focused windows."), ("border_focus_stack", "#0000ff", "Border colour for un-focused windows."), ("border_normal_stack", "#000022", "Border colour for un-focused windows."), ("grow_amount", 5, "Amount by which to grow/shrink a window."), ("border_width", 2, "Border width."), ("name", "wmii", "Name of this layout."), ("margin", 0, "Margin of the layout"), ] def __init__(self, **config): Layout.__init__(self, **config) self.add_defaults(Wmii.defaults) self.current_window = None self.clients = [] self.columns = [{'active': 0, 'width': 100, 'mode': 'split', 'rows': []}] def info(self): d = Layout.info(self) d["current_window"] = self.current_window d["clients"] = [x.name for x in self.clients] return d def add_column(self, prepend, win): newwidth = int(100 / (len(self.columns) + 1)) # we are only called if there already is a column, simplifies things for c in self.columns: c['width'] = newwidth c = {'width': newwidth, 'mode': 'split', 'rows': [win]} if prepend: self.columns.insert(0, c) else: self.columns.append(c) def clone(self, group): c = Layout.clone(self, group) c.current_window = None c.clients = [] c.columns = [{'active': 0, 'width': 100, 'mode': 'split', 'rows': []}] return c def current_column(self): if self.current_window is None: return None for c in self.columns: if self.current_window in c['rows']: return c return None def add(self, client): self.clients.append(client) c = self.current_column() if c is None: if len(self.columns) == 0: self.columns = [{'active': 0, 'width': 100, 'mode': 'split', 'rows': []}] c = self.columns[0] c['rows'].append(client) self.focus(client) def remove(self, client): if client not in self.clients: return self.clients.remove(client) for c in self.columns: if client in c['rows']: ridx = c['rows'].index(client) cidx = self.columns.index(c) c['rows'].remove(client) if len(c['rows']) != 0: if client == self.current_window: if ridx > 0: ridx -= 1 newclient = c['rows'][ridx] self.focus(newclient) self.group.focus(self.current_window) return self.current_window # column is now empty, remove it and select the previous one self.columns.remove(c) if len(self.columns) == 0: return None newwidth = int(100 / len(self.columns)) for c in self.columns: c['width'] = newwidth if len(self.columns) == 1: # there is no window at all return None if cidx > 0: cidx -= 1 c = self.columns[cidx] rows = c['rows'] newclient = rows[0] self.focus(newclient) self.group.focus(newclient) return newclient def is_last_column(self, cidx): return cidx == len(self.columns) - 1 def focus(self, client): self.current_window = client for c in self.columns: if client in c['rows']: c['active'] = c['rows'].index(client) def configure(self, client, screen): show = True if client not in self.clients: return ridx = -1 xoffset = int(screen.x) for c in self.columns: if client in c['rows']: ridx = c['rows'].index(client) break xoffset += int(float(c['width']) * screen.width / 100.0) if ridx == -1: return if client == self.current_window: if c['mode'] == 'split': px = self.group.qtile.colorPixel(self.border_focus) else: px = self.group.qtile.colorPixel(self.border_focus_stack) else: if c['mode'] == 'split': px = self.group.qtile.colorPixel(self.border_normal) else: px = self.group.qtile.colorPixel(self.border_normal_stack) if c['mode'] == 'split': oneheight = screen.height / len(c['rows']) yoffset = int(screen.y + oneheight * ridx) win_height = int(oneheight - 2 * self.border_width) else: # stacked if c['active'] != c['rows'].index(client): show = False yoffset = int(screen.y) win_height = int(screen.height - 2 * self.border_width) win_width = int(float(c['width'] * screen.width / 100.0)) win_width -= 2 * self.border_width if show: client.place( xoffset, yoffset, win_width, win_height, self.border_width, px, margin=self.margin, ) client.unhide() else: client.hide() def cmd_toggle_split(self): c = self.current_column() if c['mode'] == "split": c['mode'] = "stack" else: c['mode'] = "split" self.group.layoutAll() def focus_next(self, win): self.cmd_down() return self.curent_window def focus_previous(self, win): self.cmd_up() return self.current_window def focus_first(self): if len(self.columns) == 0: self.columns = [{'active': 0, 'width': 100, 'mode': 'split', 'rows': []}] c = self.columns[0] if len(c['rows']) != 0: return c['rows'][0] def focus_last(self): c = self.columns[len(self.columns) - 1] if len(c['rows']) != 0: return c['rows'][len(c['rows']) - 1] def cmd_left(self): """Switch to the first window on prev column""" c = self.current_column() cidx = self.columns.index(c) if cidx == 0: return cidx -= 1 c = self.columns[cidx] if c['mode'] == "split": self.group.focus(c['rows'][0]) else: self.group.focus(c['rows'][c['active']]) def cmd_right(self): """Switch to the first window on next column""" c = self.current_column() cidx = self.columns.index(c) if self.is_last_column(cidx): return cidx += 1 c = self.columns[cidx] if c['mode'] == "split": self.group.focus(c['rows'][0]) else: self.group.focus(c['rows'][c['active']]) def cmd_up(self): """Switch to the previous window in current column""" c = self.current_column() if c is None: return ridx = c['rows'].index(self.current_window) if ridx == 0: if c['mode'] != "split": ridx = len(c['rows']) - 1 else: ridx -= 1 client = c['rows'][ridx] self.group.focus(client) def cmd_down(self): """Switch to the next window in current column""" c = self.current_column() if c is None: return ridx = c['rows'].index(self.current_window) if ridx == len(c['rows']) - 1: if c['mode'] != "split": ridx = 0 else: ridx += 1 client = c['rows'][ridx] self.group.focus(client) cmd_next = cmd_down cmd_previous = cmd_up def cmd_shuffle_left(self): cur = self.current_window if cur is None: return for c in self.columns: if cur in c['rows']: cidx = self.columns.index(c) if cidx == 0: if len(c['rows']) == 1: return c['rows'].remove(cur) self.add_column(True, cur) if len(c['rows']) == 0: self.columns.remove(c) else: c['rows'].remove(cur) self.columns[cidx - 1]['rows'].append(cur) if len(c['rows']) == 0: self.columns.remove(c) newwidth = int(100 / len(self.columns)) for c in self.columns: c['width'] = newwidth else: if c['active'] >= len(c['rows']): c['active'] = len(c['rows']) - 1 self.group.focus(cur) return def swap_column_width(self, grow, shrink): grower = self.columns[grow] shrinker = self.columns[shrink] amount = self.grow_amount if shrinker['width'] - amount < 5: return grower['width'] += amount shrinker['width'] -= amount def cmd_grow_left(self): cur = self.current_window if cur is None: return for c in self.columns: if cur in c['rows']: cidx = self.columns.index(c) if cidx == 0: # grow left for leftmost-column, shrink left if self.is_last_column(cidx): return self.swap_column_width(cidx + 1, cidx) self.group.focus(cur) return self.swap_column_width(cidx, cidx - 1) self.group.focus(cur) return def cmd_grow_right(self): cur = self.current_window if cur is None: return for c in self.columns: if cur in c['rows']: cidx = self.columns.index(c) if self.is_last_column(cidx): # grow right from right most, shrink right if cidx == 0: return self.swap_column_width(cidx - 1, cidx) self.group.focus(cur) return # grow my width by 20, reduce neighbor to the right by 20 self.swap_column_width(cidx, cidx + 1) self.group.focus(cur) return def cmd_shuffle_right(self): cur = self.current_window if cur is None: return for c in self.columns: if cur in c['rows']: cidx = self.columns.index(c) if self.is_last_column(cidx): if len(c['rows']) == 1: return c['rows'].remove(cur) self.add_column(False, cur) if len(c['rows']) == 0: self.columns.remove(c) else: c['rows'].remove(cur) self.columns[cidx + 1]['rows'].append(cur) if len(c['rows']) == 0: self.columns.remove(c) newwidth = int(100 / len(self.columns)) for c in self.columns: c['width'] = newwidth else: if c['active'] >= len(c['rows']): c['active'] = len(c['rows']) - 1 self.group.focus(cur) return def cmd_shuffle_down(self): for c in self.columns: if self.current_window in c['rows']: r = c['rows'] ridx = r.index(self.current_window) if ridx + 1 < len(r): r[ridx], r[ridx + 1] = r[ridx + 1], r[ridx] client = r[ridx + 1] self.focus(client) self.group.focus(client) return def cmd_shuffle_up(self): for c in self.columns: if self.current_window in c['rows']: r = c['rows'] ridx = r.index(self.current_window) if ridx > 0: r[ridx - 1], r[ridx] = r[ridx], r[ridx - 1] client = r[ridx - 1] self.focus(client) self.group.focus(client) return qtile-0.10.7/libqtile/layout/xmonad.py000066400000000000000000000674641305063162100177150ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2011-2012 Dustin Lacewell # Copyright (c) 2011 Mounier Florian # Copyright (c) 2012 Craig Barnes # Copyright (c) 2012 Maximilian Köhl # Copyright (c) 2012, 2014-2015 Tycho Andersen # Copyright (c) 2013 jpic # Copyright (c) 2013 babadoo # Copyright (c) 2013 Jure Ham # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 dmpayton # Copyright (c) 2014 dequis # Copyright (c) 2014 Florian Scherf # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import division from .base import Layout import math class MonadTall(Layout): """Emulate the behavior of XMonad's default tiling scheme Main-Pane: A main pane that contains a single window takes up a vertical portion of the screen based on the ratio setting. This ratio can be adjusted with the ``cmd_grow_main`` and ``cmd_shrink_main`` or, while the main pane is in focus, ``cmd_grow`` and ``cmd_shrink``. :: --------------------- | | | | | | | | | | | | | | | | | | --------------------- Using the ``cmd_flip`` method will switch which horizontal side the main pane will occupy. The main pane is considered the "top" of the stack. :: --------------------- | | | | | | | | | | | | | | | | | | --------------------- Secondary-panes: Occupying the rest of the screen are one or more secondary panes. The secondary panes will share the vertical space of the screen however they can be resized at will with the ``cmd_grow`` and ``cmd_shrink`` methods. The other secondary panes will adjust their sizes to smoothly fill all of the space. :: --------------------- --------------------- | | | | |______| | |______| | | | | | | | | | | |______| | | | | | | | |______| | | | | | | --------------------- --------------------- Panes can be moved with the ``cmd_shuffle_up`` and ``cmd_shuffle_down`` methods. As mentioned the main pane is considered the top of the stack; moving up is counter-clockwise and moving down is clockwise. The opposite is true if the layout is "flipped". :: --------------------- --------------------- | | 2 | | 2 | | | |______| |_______| | | | 3 | | 3 | | | 1 |______| |_______| 1 | | | 4 | | 4 | | | | | | | | --------------------- --------------------- Normalizing: To restore all client windows to their default size ratios simply use the ``cmd_normalize`` method. Maximizing: To toggle a client window between its minimum and maximum sizes simply use the ``cmd_maximize`` on a focused client. Suggested Bindings:: Key([modkey], "h", lazy.layout.left()), Key([modkey], "l", lazy.layout.right()), Key([modkey], "j", lazy.layout.down()), Key([modkey], "k", lazy.layout.up()), Key([modkey, "shift"], "h", lazy.layout.swap_left()), Key([modkey, "shift"], "l", lazy.layout.swap_right()), Key([modkey, "shift"], "j", lazy.layout.shuffle_down()), Key([modkey, "shift"], "k", lazy.layout.shuffle_up()), Key([modkey], "i", lazy.layout.grow()), Key([modkey], "m", lazy.layout.shrink()), Key([modkey], "n", lazy.layout.normalize()), Key([modkey], "o", lazy.layout.maximize()), Key([modkey, "shift"], "space", lazy.layout.flip()), """ _left = 0 _right = 1 _min_height = 85 _min_ratio = .25 _med_ratio = .5 _max_ratio = .75 defaults = [ ("border_focus", "#ff0000", "Border colour for the focused window."), ("border_normal", "#000000", "Border colour for un-focused windows."), ("border_width", 2, "Border width."), ("single_border_width", None, "Border width for single window"), ("name", "xmonad-tall", "Name of this layout."), ("margin", 0, "Margin of the layout"), ("ratio", _med_ratio, "The percent of the screen-space the master pane should occupy " "by default."), ("align", _left, "Which side master plane will be placed " "(one of ``MonadTall._left`` or ``MonadTall._right``)"), ("change_ratio", .05, "Resize ratio"), ("change_size", 20, "Resize change in pixels"), ("new_at_current", False, "Place new windows at the position of the active window."), ] def __init__(self, **config): Layout.__init__(self, **config) self.add_defaults(MonadTall.defaults) if self.single_border_width is None: self.single_border_width = self.border_width self.clients = [] self.relative_sizes = [] self._focus = 0 # track client that has 'focus' def _get_focus(self): return self._focus def _set_focus(self, x): if len(self.clients) > 0: self._focus = abs(x % len(self.clients)) else: self._focus = 0 focused = property(_get_focus, _set_focus) def _get_relative_size_from_absolute(self, absolute_size): return absolute_size / self.group.screen.dheight def _get_absolute_size_from_relative(self, relative_size): return int(relative_size * self.group.screen.dheight) def _get_window(self): "Get currently focused client" if self.clients: return self.clients[self.focused] def focus(self, client): "Set focus to specified client" self.focused = self.clients.index(client) def clone(self, group): "Clone layout for other groups" c = Layout.clone(self, group) c.clients = [] c.sizes = [] c.relative_sizes = [] c.ratio = self.ratio c.align = self.align c._focus = 0 return c def add(self, client): "Add client to layout" new_index = self.focused + (0 if self.new_at_current else 1) self.clients.insert(new_index, client) self.do_normalize = True def remove(self, client): "Remove client from layout" if client not in self.clients: return # get index of removed client idx = self.clients.index(client) # remove the client self.clients.remove(client) # move focus pointer self.focused = max(0, idx - 1) self.do_normalize = True if self.clients: return self.clients[self.focused] def cmd_normalize(self, redraw=True): "Evenly distribute screen-space among secondary clients" n = len(self.clients) - 1 # exclude main client, 0 # if secondary clients exist if n > 0 and self.group.screen is not None: self.relative_sizes = [1.0 / n] * n # reset main pane ratio if redraw: self.group.layoutAll() self.do_normalize = False def cmd_reset(self, redraw=True): "Reset Layout." self.ratio = self._med_ratio if self.align == self._right: self.align = self._left self.cmd_normalize(redraw) def _maximize_main(self): "Toggle the main pane between min and max size" if self.ratio <= self._med_ratio: self.ratio = self._max_ratio else: self.ratio = self._min_ratio self.group.layoutAll() def _maximize_secondary(self): "Toggle the focused secondary pane between min and max size" n = len(self.clients) - 2 # total shrinking clients # total height of collapsed secondaries collapsed_height = self._min_height * n nidx = self.focused - 1 # focused size index # total height of maximized secondary maxed_size = self.group.screen.dheight - collapsed_height # if maximized or nearly maximized if abs( self._get_absolute_size_from_relative(self.relative_sizes[nidx]) - maxed_size ) < self.change_size: # minimize self._shrink_secondary( self._get_absolute_size_from_relative( self.relative_sizes[nidx] ) - self._min_height ) # otherwise maximize else: self._grow_secondary(maxed_size) def cmd_maximize(self): "Grow the currently focused client to the max size" # if we have 1 or 2 panes or main pane is focused if len(self.clients) < 3 or self.focused == 0: self._maximize_main() # secondary is focused else: self._maximize_secondary() self.group.layoutAll() def configure(self, client, screen): "Position client based on order and sizes" # if no sizes or normalize flag is set, normalize if not self.relative_sizes or self.do_normalize: self.cmd_normalize(False) # if client not in this layout if not self.clients or client not in self.clients: client.hide() return # determine focus border-color if client.has_focus: px = self.group.qtile.colorPixel(self.border_focus) else: px = self.group.qtile.colorPixel(self.border_normal) # single client - fullscreen if len(self.clients) == 1: client.place( self.group.screen.dx, self.group.screen.dy, self.group.screen.dwidth - 2 * self.single_border_width, self.group.screen.dheight - 2 * self.single_border_width, self.single_border_width, px, margin=self.margin, ) client.unhide() return cidx = self.clients.index(client) # calculate main/secondary column widths width_main = int(self.group.screen.dwidth * self.ratio) width_shared = self.group.screen.dwidth - width_main # calculate client's x offset if self.align == self._left: # left orientation if cidx == 0: # main client xpos = self.group.screen.dx else: # secondary client xpos = self.group.screen.dx + width_main else: # right orientation if cidx == 0: # main client xpos = self.group.screen.dx + width_shared else: # secondary client xpos = self.group.screen.dx # calculate client height and place if cidx > 0: # secondary client width = width_shared - 2 * self.border_width # ypos is the sum of all clients above it ypos = self.group.screen.dy + \ self._get_absolute_size_from_relative( sum(self.relative_sizes[:cidx - 1]) ) # get height from precalculated height list height = self._get_absolute_size_from_relative( self.relative_sizes[cidx - 1] ) # fix double margin if cidx > 1: ypos -= self.margin height += self.margin # place client based on calculated dimensions client.place( xpos, ypos, width, height - 2 * self.border_width, self.border_width, px, margin=self.margin, ) client.unhide() else: # main client width = width_main - 2 * self.border_width client.place( xpos + self.margin, self.group.screen.dy + self.margin, width - self.margin, self.group.screen.dheight - 2 * self.border_width - 2 * self.margin, self.border_width, px, ) client.unhide() def info(self): return { 'clients': [c.name for c in self.clients], 'main': self.clients[0].name if self.clients else None, 'secondary': [c.name for c in self.clients[1:]] } def get_shrink_margin(self, cidx): "Return how many remaining pixels a client can shrink" return max( 0, self._get_absolute_size_from_relative( self.relative_sizes[cidx] ) - self._min_height ) def shrink(self, cidx, amt): """Reduce the size of a client Will only shrink the client until it reaches the configured minimum size. Any amount that was prevented in the resize is returned. """ # get max resizable amount margin = self.get_shrink_margin(cidx) if amt > margin: # too much self.relative_sizes[cidx] -= \ self._get_relative_size_from_absolute(margin) return amt - margin else: self.relative_sizes[cidx] -= \ self._get_relative_size_from_absolute(amt) return 0 def shrink_up(self, cidx, amt): """Shrink the window up Will shrink all secondary clients above the specified index in order. Each client will attempt to shrink as much as it is able before the next client is resized. Any amount that was unable to be applied to the clients is returned. """ left = amt # track unused shrink amount # for each client before specified index for idx in range(0, cidx): # shrink by whatever is left-over of original amount left -= left - self.shrink(idx, left) # return unused shrink amount return left def shrink_up_shared(self, cidx, amt): """Shrink the shared space Will shrink all secondary clients above the specified index by an equal share of the provided amount. After applying the shared amount to all affected clients, any amount left over will be applied in a non-equal manner with ``shrink_up``. Any amount that was unable to be applied to the clients is returned. """ # split shrink amount among number of clients per_amt = amt / cidx left = amt # track unused shrink amount # for each client before specified index for idx in range(0, cidx): # shrink by equal amount and track left-over left -= per_amt - self.shrink(idx, per_amt) # apply non-equal shrinkage secondary pass # in order to use up any left over shrink amounts left = self.shrink_up(cidx, left) # return whatever could not be applied return left def shrink_down(self, cidx, amt): """Shrink current window down Will shrink all secondary clients below the specified index in order. Each client will attempt to shrink as much as it is able before the next client is resized. Any amount that was unable to be applied to the clients is returned. """ left = amt # track unused shrink amount # for each client after specified index for idx in range(cidx + 1, len(self.relative_sizes)): # shrink by current total left-over amount left -= left - self.shrink(idx, left) # return unused shrink amount return left def shrink_down_shared(self, cidx, amt): """Shrink secondary clients Will shrink all secondary clients below the specified index by an equal share of the provided amount. After applying the shared amount to all affected clients, any amount left over will be applied in a non-equal manner with ``shrink_down``. Any amount that was unable to be applied to the clients is returned. """ # split shrink amount among number of clients per_amt = amt / (len(self.relative_sizes) - 1 - cidx) left = amt # track unused shrink amount # for each client after specified index for idx in range(cidx + 1, len(self.relative_sizes)): # shrink by equal amount and track left-over left -= per_amt - self.shrink(idx, per_amt) # apply non-equal shrinkage secondary pass # in order to use up any left over shrink amounts left = self.shrink_down(cidx, left) # return whatever could not be applied return left def _grow_main(self, amt): """Will grow the client that is currently in the main pane""" self.ratio += amt self.ratio = min(self._max_ratio, self.ratio) def _grow_solo_secondary(self, amt): """Will grow the solitary client in the secondary pane""" self.ratio -= amt self.ratio = max(self._min_ratio, self.ratio) def _grow_secondary(self, amt): """Will grow the focused client in the secondary pane""" half_change_size = amt / 2 # track unshrinkable amounts left = amt # first secondary (top) if self.focused == 1: # only shrink downwards left -= amt - self.shrink_down_shared(0, amt) # last secondary (bottom) elif self.focused == len(self.clients) - 1: # only shrink upwards left -= amt - self.shrink_up(len(self.relative_sizes) - 1, amt) # middle secondary else: # get size index idx = self.focused - 1 # shrink up and down left -= half_change_size - self.shrink_up_shared( idx, half_change_size ) left -= half_change_size - self.shrink_down_shared( idx, half_change_size ) left -= half_change_size - self.shrink_up_shared( idx, half_change_size ) left -= half_change_size - self.shrink_down_shared( idx, half_change_size ) # calculate how much shrinkage took place diff = amt - left # grow client by diff amount self.relative_sizes[self.focused - 1] += \ self._get_relative_size_from_absolute(diff) def cmd_grow(self): """Grow current window Will grow the currently focused client reducing the size of those around it. Growing will stop when no other secondary clients can reduce their size any further. """ if self.focused == 0: self._grow_main(self.change_ratio) elif len(self.clients) == 2: self._grow_solo_secondary(self.change_ratio) else: self._grow_secondary(self.change_size) self.group.layoutAll() def cmd_grow_main(self): """Grow main pane Will grow the main pane, reducing the size of clients in the secondary pane. """ self._grow_main(self.change_ratio) self.group.layoutAll() def cmd_shrink_main(self): """Shrink main pane Will shrink the main pane, increasing the size of clients in the secondary pane. """ self._shrink_main(self.change_ratio) self.group.layoutAll() def grow(self, cidx, amt): "Grow secondary client by specified amount" self.relative_sizes[cidx] += self._get_relative_size_from_absolute(amt) def grow_up_shared(self, cidx, amt): """Grow higher secondary clients Will grow all secondary clients above the specified index by an equal share of the provided amount. """ # split grow amount among number of clients per_amt = amt / cidx for idx in range(0, cidx): self.grow(idx, per_amt) def grow_down_shared(self, cidx, amt): """Grow lower secondary clients Will grow all secondary clients below the specified index by an equal share of the provided amount. """ # split grow amount among number of clients per_amt = amt / (len(self.relative_sizes) - 1 - cidx) for idx in range(cidx + 1, len(self.relative_sizes)): self.grow(idx, per_amt) def _shrink_main(self, amt): """Will shrink the client that currently in the main pane""" self.ratio -= amt self.ratio = max(self._min_ratio, self.ratio) def _shrink_solo_secondary(self, amt): """Will shrink the solitary client in the secondary pane""" self.ratio += amt self.ratio = min(self._max_ratio, self.ratio) def _shrink_secondary(self, amt): """Will shrink the focused client in the secondary pane""" # get focused client client = self.clients[self.focused] # get default change size change = amt # get left-over height after change left = client.height - amt # if change would violate min_height if left < self._min_height: # just reduce to min_height change = client.height - self._min_height # calculate half of that change half_change = change / 2 # first secondary (top) if self.focused == 1: # only grow downwards self.grow_down_shared(0, change) # last secondary (bottom) elif self.focused == len(self.clients) - 1: # only grow upwards self.grow_up_shared(len(self.relative_sizes) - 1, change) # middle secondary else: idx = self.focused - 1 # grow up and down self.grow_up_shared(idx, half_change) self.grow_down_shared(idx, half_change) # shrink client by total change self.relative_sizes[self.focused - 1] -= \ self._get_relative_size_from_absolute(change) def focus_first(self): if self.clients: return self.clients[0] def focus_last(self): if self.clients: return self.clients[-1] def focus_next(self, window): if not self.clients: return if self.focused + 1 < len(self.clients): return self.clients[self.focused + 1] def focus_previous(self, window): if not self.clients: return if self.focused > 0: return self.clients[self.focused - 1] def cmd_next(self): client = self.focus_next(self.clients[self.focused]) or \ self.focus_first() self.group.focus(client) def cmd_previous(self): client = self.focus_previous(self.clients[self.focused]) or \ self.focus_last() self.group.focus(client) def cmd_shrink(self): """Shrink current window Will shrink the currently focused client reducing the size of those around it. Shrinking will stop when the client has reached the minimum size. """ if self.focused == 0: self._shrink_main(self.change_ratio) elif len(self.clients) == 2: self._shrink_solo_secondary(self.change_ratio) else: self._shrink_secondary(self.change_size) self.group.layoutAll() def cmd_up(self): """Focus on the next more prominent client on the stack""" self.focused -= 1 self.group.focus(self.clients[self.focused]) def cmd_down(self): """Focus on the less prominent client on the stack""" self.focused += 1 self.group.focus(self.clients[self.focused]) def cmd_shuffle_up(self): """Shuffle the client up the stack""" _oldf = self.focused self.focused -= 1 self.clients[_oldf], self.clients[self.focused] = \ self.clients[self.focused], self.clients[_oldf] self.group.layoutAll() self.group.focus(self.clients[self.focused]) def cmd_shuffle_down(self): """Shuffle the client down the stack""" _oldf = self.focused self.focused += 1 self.clients[_oldf], self.clients[self.focused] = \ self.clients[self.focused], self.clients[_oldf] self.group.layoutAll() self.group.focus(self.clients[self.focused]) def cmd_flip(self): """Flip the layout horizontally""" self.align = self._left if self.align == self._right else self._right self.group.layoutAll() def _get_closest(self, x, y, clients): """Get closest window to a point x,y""" target = min( clients, key=lambda c: math.hypot(c.info()['x'] - x, c.info()['y'] - y) ) return target def cmd_swap(self, window1, window2): """Swap two windows""" index1 = self.clients.index(window1) index2 = self.clients.index(window2) self.clients[index1], self.clients[index2] = \ self.clients[index2], self.clients[index1] self.group.layoutAll() self.focused = index1 self.group.focus(window1) def cmd_swap_left(self): """Swap current window with closest window to the left""" x = self._get_window().x y = self._get_window().y candidates = [c for c in self.clients if c.info()['x'] < x] target = self._get_closest(x, y, candidates) self.cmd_swap(self._get_window(), target) def cmd_swap_right(self): """Swap current window with closest window to the right""" x = self._get_window().x y = self. _get_window().y candidates = [c for c in self.clients if c.info()['x'] > x] target = self._get_closest(x, y, candidates) self.cmd_swap(self._get_window(), target) def cmd_swap_main(self): """Swap current window to main pane""" if self.align == self._left: self.cmd_swap_left() elif self.align == self._right: self.cmd_swap_right() def cmd_left(self): """Focus on the closest window to the left of the current window""" x = self._get_window().x y = self._get_window().y candidates = [c for c in self.clients if c.info()['x'] < x] target = self._get_closest(x, y, candidates) self.focused = self.clients.index(target) self.group.focus(self.clients[self.focused]) def cmd_right(self): """Focus on the closest window to the right of the current window""" x = self._get_window().x y = self._get_window().y candidates = [c for c in self.clients if c.info()['x'] > x] target = self._get_closest(x, y, candidates) self.focused = self.clients.index(target) self.group.focus(self.clients[self.focused]) qtile-0.10.7/libqtile/layout/zoomy.py000066400000000000000000000127661305063162100175770ustar00rootroot00000000000000# Copyright (c) 2011 Mounier Florian # Copyright (c) 2011 Paul Colomiets # Copyright (c) 2012 Craig Barnes # Copyright (c) 2012, 2014 Tycho Andersen # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 dmpayton # Copyright (c) 2014 dequis # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import division from .base import Layout class Zoomy(Layout): """A layout with single active windows, and few other previews at the right""" defaults = [ ("columnwidth", 150, "Width of the right column"), ("property_name", "ZOOM", "Property to set on zoomed window"), ("property_small", "0.1", "Property value to set on zoomed window"), ("property_big", "1.0", "Property value to set on normal window"), ("margin", 0, "Margin of the layout"), ] def __init__(self, **config): Layout.__init__(self, **config) self.add_defaults(Zoomy.defaults) self.clients = [] self.focused = None def _get_window(self): return self.focused def focus_first(self): if self.clients: return self.clients[0] def focus_last(self): if self.clients: return self.clients[-1] def focus_next(self, client): if client not in self.clients: return idx = self.clients.index(client) return self.clients[(idx + 1) % len(self.clients)] def focus_previous(self, client): if not self.clients: return idx = self.clients.index(client) return self.clients[idx - 1] def clone(self, group): c = Layout.clone(self, group) c.clients = [] return c def add(self, client): self.clients.insert(0, client) self.focus(client) def remove(self, client): if client not in self.clients: return if self.focused == client: self.focused = self.focus_previous(client) if self.focused == client: self.focused = None self.clients.remove(client) return self.focused def configure(self, client, screen): left, right = screen.hsplit(screen.width - self.columnwidth) if client is self.focused: client.place( left.x, left.y, left.width, left.height, 0, None, margin=self.margin, ) else: h = right.width * left.height // left.width client_index = self.clients.index(client) focused_index = self.clients.index(self.focused) offset = client_index - focused_index - 1 if offset < 0: offset += len(self.clients) if h * (len(self.clients) - 1) < right.height: client.place( right.x, right.y + h * offset, right.width, h, 0, None, margin=self.margin, ) else: hh = (right.height - h) // (len(self.clients) - 1) client.place( right.x, right.y + hh * offset, right.width, h, 0, None, margin=self.margin, ) client.unhide() def info(self): d = Layout.info(self) d["clients"] = [x.name for x in self.clients] return d def focus(self, win): if self.focused and self.property_name and self.focused.window.get_property( self.property_name, "UTF8_STRING" ) is not None: self.focused.window.set_property( self.property_name, self.property_small, "UTF8_STRING", format=8 ) Layout.focus(self, win) if self.property_name: self.focused = win win.window.set_property( self.property_name, self.property_big, "UTF8_STRING", format=8 ) def cmd_next(self): client = self.focus_next(self.focused) or self.focus_first() self.group.focus(client, False) cmd_down = cmd_next def cmd_previous(self): client = self.focus_previous(self.focused) or self.focus_last() self.group.focus(client, False) cmd_up = cmd_previous qtile-0.10.7/libqtile/log_utils.py000066400000000000000000000107611305063162100170770ustar00rootroot00000000000000# Copyright (c) 2012 Florian Mounier # Copyright (c) 2013-2014 Tao Sauvage # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 roger # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from logging import getLogger, StreamHandler, Formatter, WARNING, captureWarnings from logging.handlers import RotatingFileHandler import os import sys import warnings logger = getLogger(__package__) class ColorFormatter(Formatter): """Logging formatter adding console colors to the output.""" black, red, green, yellow, blue, magenta, cyan, white = range(8) colors = { 'WARNING': yellow, 'INFO': green, 'DEBUG': blue, 'CRITICAL': yellow, 'ERROR': red, 'RED': red, 'GREEN': green, 'YELLOW': yellow, 'BLUE': blue, 'MAGENTA': magenta, 'CYAN': cyan, 'WHITE': white } reset_seq = '\033[0m' color_seq = '\033[%dm' bold_seq = '\033[1m' def format(self, record): """Format the record with colors.""" color = self.color_seq % (30 + self.colors[record.levelname]) message = Formatter.format(self, record) message = message.replace('$RESET', self.reset_seq)\ .replace('$BOLD', self.bold_seq)\ .replace('$COLOR', color) for color, value in self.colors.items(): message = message.replace( '$' + color, self.color_seq % (value + 30))\ .replace('$BG' + color, self.color_seq % (value + 40))\ .replace('$BG-' + color, self.color_seq % (value + 40)) return message + self.reset_seq def init_log(log_level=WARNING, log_path=True, log_truncate=False, log_size=10000000, log_numbackups=1, log_color=True): formatter = Formatter( "%(asctime)s %(levelname)s %(name)s %(filename)s:%(funcName)s():L%(lineno)d %(message)s" ) # We'll always use a stream handler stream_handler = StreamHandler(sys.stdout) if log_color: color_formatter = ColorFormatter( '$RESET$COLOR%(asctime)s $BOLD$COLOR%(name)s %(filename)s:%(funcName)s():L%(lineno)d $RESET %(message)s' ) stream_handler.setFormatter(color_formatter) else: stream_handler.setFormatter(formatter) logger.addHandler(stream_handler) # If we have a log path, we'll also setup a log file if log_path: if not isinstance(log_path, str): data_directory = os.path.expandvars('$XDG_DATA_HOME') if data_directory == '$XDG_DATA_HOME': # if variable wasn't set data_directory = os.path.expanduser("~/.local/share") data_directory = os.path.join(data_directory, 'qtile') if not os.path.exists(data_directory): os.makedirs(data_directory) log_path = os.path.join(data_directory, '%s.log') try: log_path %= 'qtile' except TypeError: # Happens if log_path doesn't contain formatters. pass log_path = os.path.expanduser(log_path) if log_truncate: with open(log_path, "w"): pass file_handler = RotatingFileHandler( log_path, maxBytes=log_size, backupCount=log_numbackups ) file_handler.setFormatter(formatter) logger.addHandler(file_handler) logger.setLevel(log_level) # Capture everything from the warnings module. captureWarnings(True) warnings.simplefilter("always") logger.warning('Starting logging for Qtile') return logger qtile-0.10.7/libqtile/manager.py000066400000000000000000001743741305063162100165230ustar00rootroot00000000000000# Copyright (c) 2008, Aldo Cortesi. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import division try: import tracemalloc except ImportError: tracemalloc = None from libqtile.dgroups import DGroups from xcffib.xproto import EventMask, WindowError, AccessError, DrawableError import io import logging import os import pickle import shlex import signal import sys import traceback import xcffib import xcffib.xinerama import xcffib.xproto import six import warnings from . import asyncio from .config import Drag, Click, Screen, Match, Rule from .group import _Group from .log_utils import logger from .state import QtileState from .utils import QtileError, get_cache_dir from .widget.base import _Widget from . import command from . import hook from . import utils from . import window from . import xcbq if sys.version_info >= (3, 3): def _import_module(module_name, dir_path): import importlib file_name = os.path.join(dir_path, module_name) + '.py' f = importlib.machinery.SourceFileLoader(module_name, file_name) module = f.load_module() return module else: def _import_module(module_name, dir_path): import imp fp = None try: fp, pathname, description = imp.find_module(module_name, [dir_path]) module = imp.load_module(module_name, fp, pathname, description) finally: if fp: fp.close() return module class Qtile(command.CommandObject): """This object is the `root` of the command graph""" def __init__(self, config, displayName=None, fname=None, no_spawn=False, state=None): self.no_spawn = no_spawn self._eventloop = None self._finalize = False if not displayName: displayName = os.environ.get("DISPLAY") if not displayName: raise QtileError("No DISPLAY set.") if not fname: # Dots might appear in the host part of the display name # during remote X sessions. Let's strip the host part first. displayNum = displayName.partition(":")[2] if "." not in displayNum: displayName += ".0" fname = command.find_sockfile(displayName) self.conn = xcbq.Connection(displayName) self.config = config self.fname = fname hook.init(self) self.windowMap = {} self.widgetMap = {} self.groupMap = {} self.groups = [] self.keyMap = {} # Find the modifier mask for the numlock key, if there is one: nc = self.conn.keysym_to_keycode(xcbq.keysyms["Num_Lock"]) self.numlockMask = xcbq.ModMasks.get(self.conn.get_modifier(nc), 0) self.validMask = ~(self.numlockMask | xcbq.ModMasks["lock"]) # Because we only do Xinerama multi-screening, # we can assume that the first # screen's root is _the_ root. self.root = self.conn.default_screen.root self.root.set_attribute( eventmask=( EventMask.StructureNotify | EventMask.SubstructureNotify | EventMask.SubstructureRedirect | EventMask.EnterWindow | EventMask.LeaveWindow ) ) self.root.set_property( '_NET_SUPPORTED', [self.conn.atoms[x] for x in xcbq.SUPPORTED_ATOMS] ) self.supporting_wm_check_window = self.conn.create_window(-1, -1, 1, 1) self.root.set_property( '_NET_SUPPORTING_WM_CHECK', self.supporting_wm_check_window.wid ) # setup the default cursor self.root.set_cursor('left_ptr') wmname = getattr(self.config, "wmname", "qtile") self.supporting_wm_check_window.set_property('_NET_WM_NAME', wmname) self.supporting_wm_check_window.set_property( '_NET_SUPPORTING_WM_CHECK', self.supporting_wm_check_window.wid ) if config.main: config.main(self) self.dgroups = None if self.config.groups: key_binder = None if hasattr(self.config, 'dgroups_key_binder'): key_binder = self.config.dgroups_key_binder self.dgroups = DGroups(self, self.config.groups, key_binder) if hasattr(config, "widget_defaults") and config.widget_defaults: _Widget.global_defaults = config.widget_defaults else: _Widget.global_defaults = {} for i in self.groups: self.groupMap[i.name] = i self.setup_eventloop() self.server = command._Server(self.fname, self, config, self._eventloop) self.currentScreen = None self.screens = [] self._process_screens() self.currentScreen = self.screens[0] self._drag = None self.ignoreEvents = set([ xcffib.xproto.KeyReleaseEvent, xcffib.xproto.ReparentNotifyEvent, xcffib.xproto.CreateNotifyEvent, # DWM handles this to help "broken focusing windows". xcffib.xproto.MapNotifyEvent, xcffib.xproto.LeaveNotifyEvent, xcffib.xproto.FocusOutEvent, xcffib.xproto.FocusInEvent, xcffib.xproto.NoExposureEvent ]) self.conn.flush() self.conn.xsync() self._xpoll() # Map and Grab keys for key in self.config.keys: self.mapKey(key) # It fixes problems with focus when clicking windows of some specific clients like xterm def noop(qtile): pass self.config.mouse += (Click([], "Button1", command.lazy.function(noop), focus="after"),) self.mouseMap = {} for i in self.config.mouse: if self.mouseMap.get(i.button_code) is None: self.mouseMap[i.button_code] = [] self.mouseMap[i.button_code].append(i) self.grabMouse() # no_spawn is set when we are restarting; we only want to run the # startup hook once. if not no_spawn: hook.fire("startup_once") hook.fire("startup") if state: st = pickle.load(io.BytesIO(state.encode())) try: st.apply(self) except: logger.exception("failed restoring state") self.scan() self.update_net_desktops() hook.subscribe.setgroup(self.update_net_desktops) self.selection = { "PRIMARY": {"owner": None, "selection": ""}, "CLIPBOARD": {"owner": None, "selection": ""} } self.setup_selection() hook.fire("startup_complete") def setup_selection(self): PRIMARY = self.conn.atoms["PRIMARY"] CLIPBOARD = self.conn.atoms["CLIPBOARD"] self.selection_window = self.conn.create_window(-1, -1, 1, 1) self.selection_window.set_attribute(eventmask=EventMask.PropertyChange) self.conn.xfixes.select_selection_input(self.selection_window, "PRIMARY") self.conn.xfixes.select_selection_input(self.selection_window, "CLIPBOARD") r = self.conn.conn.core.GetSelectionOwner(PRIMARY).reply() self.selection["PRIMARY"]["owner"] = r.owner r = self.conn.conn.core.GetSelectionOwner(CLIPBOARD).reply() self.selection["CLIPBOARD"]["owner"] = r.owner # ask for selection on starup self.convert_selection(PRIMARY) self.convert_selection(CLIPBOARD) def setup_eventloop(self): self._eventloop = asyncio.new_event_loop() self._eventloop.add_signal_handler(signal.SIGINT, self.stop) self._eventloop.add_signal_handler(signal.SIGTERM, self.stop) self._eventloop.set_exception_handler( lambda x, y: logger.exception("Got an exception in poll loop") ) logger.info('Adding io watch') fd = self.conn.conn.get_file_descriptor() self._eventloop.add_reader(fd, self._xpoll) self.setup_python_dbus() def setup_python_dbus(self): # This is a little strange. python-dbus internally depends on gobject, # so gobject's threads need to be running, and a gobject "main loop # thread" needs to be spawned, but we try to let it only interact with # us via calls to asyncio's call_soon_threadsafe. try: # We import dbus here to thrown an ImportError if it isn't # available. Since the only reason we're running this thread is # because of dbus, if dbus isn't around there's no need to run # this thread. import dbus # noqa from gi.repository import GLib def gobject_thread(): ctx = GLib.main_context_default() while not self._finalize: try: ctx.iteration(True) except Exception: logger.exception("got exception from gobject") self._glib_loop = self.run_in_executor(gobject_thread) except ImportError: logger.warning("importing dbus/gobject failed, dbus will not work.") self._glib_loop = None def finalize(self): self._finalize = True self._eventloop.remove_signal_handler(signal.SIGINT) self._eventloop.remove_signal_handler(signal.SIGTERM) self._eventloop.set_exception_handler(None) if self._glib_loop: try: from gi.repository import GLib GLib.idle_add(lambda: None) self._eventloop.run_until_complete(self._glib_loop) except ImportError: pass try: for w in self.widgetMap.values(): w.finalize() for l in self.config.layouts: l.finalize() for screen in self.screens: for bar in [screen.top, screen.bottom, screen.left, screen.right]: if bar is not None: bar.finalize() logger.info('Removing io watch') fd = self.conn.conn.get_file_descriptor() self._eventloop.remove_reader(fd) self.conn.finalize() self.server.close() except: logger.exception('exception during finalize') finally: self._eventloop.close() self._eventloop = None def _process_fake_screens(self): """ Since Xephyr and Xnest don't really support offset screens, we'll fake it here for testing, (or if you want to partition a physical monitor into separate screens) """ for i, s in enumerate(self.config.fake_screens): # should have x,y, width and height set s._configure(self, i, s.x, s.y, s.width, s.height, self.groups[i]) if not self.currentScreen: self.currentScreen = s self.screens.append(s) def _process_screens(self): if hasattr(self.config, 'fake_screens'): self._process_fake_screens() return # What's going on here is a little funny. What we really want is only # screens that don't overlap here; overlapping screens should see the # same parts of the root window (i.e. for people doing xrandr # --same-as). However, the order that X gives us pseudo screens in is # important, because it indicates what people have chosen via xrandr # --primary or whatever. So we need to alias screens that should be # aliased, but preserve order as well. See #383. xywh = {} screenpos = [] for s in self.conn.pseudoscreens: pos = (s.x, s.y) (w, h) = xywh.get(pos, (0, 0)) if pos not in xywh: screenpos.append(pos) xywh[pos] = (max(w, s.width), max(h, s.height)) for i, (x, y) in enumerate(screenpos): (w, h) = xywh[(x, y)] if i + 1 > len(self.config.screens): scr = Screen() else: scr = self.config.screens[i] if not self.currentScreen: self.currentScreen = scr scr._configure( self, i, x, y, w, h, self.groups[i], ) self.screens.append(scr) if not self.screens: if self.config.screens: s = self.config.screens[0] else: s = Screen() self.currentScreen = s s._configure( self, 0, 0, 0, self.conn.default_screen.width_in_pixels, self.conn.default_screen.height_in_pixels, self.groups[0], ) self.screens.append(s) def mapKey(self, key): self.keyMap[(key.keysym, key.modmask & self.validMask)] = key code = self.conn.keysym_to_keycode(key.keysym) self.root.grab_key( code, key.modmask, True, xcffib.xproto.GrabMode.Async, xcffib.xproto.GrabMode.Async, ) if self.numlockMask: self.root.grab_key( code, key.modmask | self.numlockMask, True, xcffib.xproto.GrabMode.Async, xcffib.xproto.GrabMode.Async, ) self.root.grab_key( code, key.modmask | self.numlockMask | xcbq.ModMasks["lock"], True, xcffib.xproto.GrabMode.Async, xcffib.xproto.GrabMode.Async, ) def unmapKey(self, key): key_index = (key.keysym, key.modmask & self.validMask) if key_index not in self.keyMap: return code = self.conn.keysym_to_keycode(key.keysym) self.root.ungrab_key(code, key.modmask) if self.numlockMask: self.root.ungrab_key(code, key.modmask | self.numlockMask) self.root.ungrab_key( code, key.modmask | self.numlockMask | xcbq.ModMasks["lock"] ) del(self.keyMap[key_index]) def update_net_desktops(self): try: index = self.groups.index(self.currentGroup) # TODO: we should really only except ValueError here, AttributeError is # an annoying chicken and egg because we're accessing currentScreen # (via currentGroup), and when we set up the initial groups, there # aren't any screens yet. This can probably be changed when #475 is # fixed. except (ValueError, AttributeError): index = 0 self.root.set_property("_NET_NUMBER_OF_DESKTOPS", len(self.groups)) self.root.set_property( "_NET_DESKTOP_NAMES", "\0".join([i.name for i in self.groups]) ) self.root.set_property("_NET_CURRENT_DESKTOP", index) def addGroup(self, name, layout=None, layouts=None): if name not in self.groupMap.keys(): g = _Group(name, layout) self.groups.append(g) if not layouts: layouts = self.config.layouts g._configure(layouts, self.config.floating_layout, self) self.groupMap[name] = g hook.fire("addgroup", self, name) hook.fire("changegroup") self.update_net_desktops() return True return False def delGroup(self, name): # one group per screen is needed if len(self.groups) == len(self.screens): raise ValueError("Can't delete all groups.") if name in self.groupMap.keys(): group = self.groupMap[name] if group.screen and group.screen.previous_group: target = group.screen.previous_group else: target = group.prevGroup() # Find a group that's not currently on a screen to bring to the # front. This will terminate because of our check above. while target.screen: target = target.prevGroup() for i in list(group.windows): i.togroup(target.name) if self.currentGroup.name == name: self.currentScreen.setGroup(target, save_prev=False) self.groups.remove(group) del(self.groupMap[name]) hook.fire("delgroup", self, name) hook.fire("changegroup") self.update_net_desktops() def registerWidget(self, w): """Register a bar widget If a widget with the same name already exists, this will silently ignore that widget. However, this is not necessarily a bug. By default a widget's name is just ``self.__class__.lower()``, so putting multiple widgets of the same class will alias and one will be inaccessible. Since more than one groupbox widget is useful when you have more than one screen, this is a not uncommon occurrence. If you want to use the debug info for widgets with the same name, set the name yourself. """ if w.name: if w.name in self.widgetMap: return self.widgetMap[w.name] = w @utils.lru_cache() def colorPixel(self, name): return self.conn.screens[0].default_colormap.alloc_color(name).pixel @property def currentLayout(self): return self.currentGroup.layout @property def currentGroup(self): return self.currentScreen.group @property def currentWindow(self): return self.currentScreen.group.currentWindow def scan(self): _, _, children = self.root.query_tree() for item in children: try: attrs = item.get_attributes() state = item.get_wm_state() except (xcffib.xproto.WindowError, xcffib.xproto.AccessError): continue if attrs and attrs.map_state == xcffib.xproto.MapState.Unmapped: continue if state and state[0] == window.WithdrawnState: continue self.manage(item) def unmanage(self, win): c = self.windowMap.get(win) if c: hook.fire("client_killed", c) self.reset_gaps(c) if getattr(c, "group", None): c.group.remove(c) del self.windowMap[win] self.update_client_list() def reset_gaps(self, c): if c.strut: self.update_gaps((0, 0, 0, 0), c.strut) def update_gaps(self, strut, old_strut=None): from libqtile.bar import Gap (left, right, top, bottom) = strut[:4] if old_strut: (old_left, old_right, old_top, old_bottom) = old_strut[:4] if not left and old_left: self.currentScreen.left = None elif not right and old_right: self.currentScreen.right = None elif not top and old_top: self.currentScreen.top = None elif not bottom and old_bottom: self.currentScreen.bottom = None if top: self.currentScreen.top = Gap(top) elif bottom: self.currentScreen.bottom = Gap(bottom) elif left: self.currentScreen.left = Gap(left) elif right: self.currentScreen.right = Gap(right) self.currentScreen.resize() def manage(self, w): try: attrs = w.get_attributes() internal = w.get_property("QTILE_INTERNAL") except (xcffib.xproto.WindowError, xcffib.xproto.AccessError): return if attrs and attrs.override_redirect: return if w.wid not in self.windowMap: if internal: try: c = window.Internal(w, self) except (xcffib.xproto.WindowError, xcffib.xproto.AccessError): return self.windowMap[w.wid] = c else: try: c = window.Window(w, self) except (xcffib.xproto.WindowError, xcffib.xproto.AccessError): return if w.get_wm_type() == "dock" or c.strut: c.static(self.currentScreen.index) else: hook.fire("client_new", c) # Window may be defunct because # it's been declared static in hook. if c.defunct: return self.windowMap[w.wid] = c # Window may have been bound to a group in the hook. if not c.group: self.currentScreen.group.add(c, focus=c.can_steal_focus()) self.update_client_list() hook.fire("client_managed", c) return c else: return self.windowMap[w.wid] def update_client_list(self): """Updates the client stack list This is needed for third party tasklists and drag and drop of tabs in chrome """ windows = [wid for wid, c in self.windowMap.items() if c.group] self.root.set_property("_NET_CLIENT_LIST", windows) # TODO: check stack order self.root.set_property("_NET_CLIENT_LIST_STACKING", windows) def grabMouse(self): self.root.ungrab_button(None, None) for i in self.config.mouse: if isinstance(i, Click) and i.focus: # Make a freezing grab on mouse button to gain focus # Event will propagate to target window grabmode = xcffib.xproto.GrabMode.Sync else: grabmode = xcffib.xproto.GrabMode.Async eventmask = EventMask.ButtonPress if isinstance(i, Drag): eventmask |= EventMask.ButtonRelease self.root.grab_button( i.button_code, i.modmask, True, eventmask, grabmode, xcffib.xproto.GrabMode.Async, ) if self.numlockMask: self.root.grab_button( i.button_code, i.modmask | self.numlockMask, True, eventmask, grabmode, xcffib.xproto.GrabMode.Async, ) self.root.grab_button( i.button_code, i.modmask | self.numlockMask | xcbq.ModMasks["lock"], True, eventmask, grabmode, xcffib.xproto.GrabMode.Async, ) def grabKeys(self): self.root.ungrab_key(None, None) for key in self.keyMap.values(): self.mapKey(key) def get_target_chain(self, ename, e): """Returns a chain of targets that can handle this event Finds functions named `handle_X`, either on the window object itself or on the Qtile instance, where X is the event name (e.g. EnterNotify, ConfigureNotify, etc). The event will be passed to each target in turn for handling, until one of the handlers returns False or None, or the end of the chain is reached. """ chain = [] handler = "handle_%s" % ename # Certain events expose the affected window id as an "event" attribute. eventEvents = [ "EnterNotify", "ButtonPress", "ButtonRelease", "KeyPress", ] if hasattr(e, "window"): c = self.windowMap.get(e.window) elif hasattr(e, "drawable"): c = self.windowMap.get(e.drawable) elif ename in eventEvents: c = self.windowMap.get(e.event) else: c = None if c is not None and hasattr(c, handler): chain.append(getattr(c, handler)) if hasattr(self, handler): chain.append(getattr(self, handler)) if not chain: logger.info("Unhandled event: %r" % ename) return chain def _xpoll(self): while True: try: e = self.conn.conn.poll_for_event() if not e: break ename = e.__class__.__name__ if ename.endswith("Event"): ename = ename[:-5] if e.__class__ not in self.ignoreEvents: logger.debug(ename) for h in self.get_target_chain(ename, e): logger.info("Handling: %s" % ename) r = h(e) if not r: break # Catch some bad X exceptions. Since X is event based, race # conditions can occur almost anywhere in the code. For # example, if a window is created and then immediately # destroyed (before the event handler is evoked), when the # event handler tries to examine the window properties, it # will throw a WindowError exception. We can essentially # ignore it, since the window is already dead and we've got # another event in the queue notifying us to clean it up. except (WindowError, AccessError, DrawableError): pass except Exception as e: error_code = self.conn.conn.has_error() if error_code: error_string = xcbq.XCB_CONN_ERRORS[error_code] logger.exception("Shutting down due to X connection error %s (%s)" % (error_string, error_code)) self.stop() return logger.exception("Got an exception in poll loop") self.conn.flush() def stop(self): logger.info('Stopping eventloop') self._eventloop.stop() def loop(self): self.server.start() try: self._eventloop.run_forever() finally: self.finalize() def find_screen(self, x, y): """Find a screen based on the x and y offset""" result = [] for i in self.screens: if i.x <= x <= i.x + i.width and \ i.y <= y <= i.y + i.height: result.append(i) if len(result) == 1: return result[0] return None def find_closest_screen(self, x, y): """ If find_screen returns None, then this basically extends a screen vertically and horizontally and see if x,y lies in the band. Only works if it can find a SINGLE closest screen, else we revert to _find_closest_closest. Useful when dragging a window out of a screen onto another but having leftmost corner above viewport. """ normal = self.find_screen(x, y) if normal is not None: return normal x_match = [] y_match = [] for i in self.screens: if i.x <= x <= i.x + i.width: x_match.append(i) if i.y <= y <= i.y + i.height: y_match.append(i) if len(x_match) == 1: return x_match[0] if len(y_match) == 1: return y_match[0] return self._find_closest_closest(x, y, x_match + y_match) def _find_closest_closest(self, x, y, candidate_screens): """ if find_closest_screen can't determine one, we've got multiple screens, so figure out who is closer. We'll calculate using the square of the distance from the center of a screen. Note that this could return None if x, y is right/below all screens (shouldn't happen but we don't do anything about it here other than returning None) """ closest_distance = None closest_screen = None if not candidate_screens: # try all screens candidate_screens = self.screens # if left corner is below and right of screen # it can't really be a candidate candidate_screens = [ s for s in candidate_screens if x < s.x + s.width and y < s.y + s.height ] for s in candidate_screens: middle_x = s.x + s.width / 2 middle_y = s.y + s.height / 2 distance = (x - middle_x) ** 2 + (y - middle_y) ** 2 if closest_distance is None or distance < closest_distance: closest_distance = distance closest_screen = s return closest_screen def handle_SelectionNotify(self, e): if not getattr(e, "owner", None): return name = self.conn.atoms.get_name(e.selection) self.selection[name]["owner"] = e.owner self.selection[name]["selection"] = "" self.convert_selection(e.selection) hook.fire("selection_notify", name, self.selection[name]) def convert_selection(self, selection, _type="UTF8_STRING"): TYPE = self.conn.atoms[_type] self.conn.conn.core.ConvertSelection(self.selection_window.wid, selection, TYPE, selection, xcffib.CurrentTime) def handle_PropertyNotify(self, e): name = self.conn.atoms.get_name(e.atom) # it's the selection property if name in ("PRIMARY", "CLIPBOARD"): assert e.window == self.selection_window.wid prop = self.selection_window.get_property(e.atom, "UTF8_STRING") # If the selection property is None, it is unset, which means the # clipboard is empty. value = prop and prop.value.to_utf8() or u"" self.selection[name]["selection"] = value hook.fire("selection_change", name, self.selection[name]) def handle_EnterNotify(self, e): if e.event in self.windowMap: return True s = self.find_screen(e.root_x, e.root_y) if s: self.toScreen(s.index, warp=False) def handle_ClientMessage(self, event): atoms = self.conn.atoms opcode = event.type data = event.data # handle change of desktop if atoms["_NET_CURRENT_DESKTOP"] == opcode: index = data.data32[0] try: self.currentScreen.setGroup(self.groups[index]) except IndexError: logger.info("Invalid Desktop Index: %s" % index) def handle_KeyPress(self, e): keysym = self.conn.code_to_syms[e.detail][0] state = e.state if self.numlockMask: state = e.state | self.numlockMask k = self.keyMap.get((keysym, state & self.validMask)) if not k: logger.info("Ignoring unknown keysym: %s" % keysym) return for i in k.commands: if i.check(self): status, val = self.server.call( (i.selectors, i.name, i.args, i.kwargs) ) if status in (command.ERROR, command.EXCEPTION): logger.error("KB command error %s: %s" % (i.name, val)) else: return def cmd_focus_by_click(self, e): """Bring a window to the front Parameters ========== e : xcb event Click event used to determine window to focus """ wnd = e.child or e.root # Additional option for config.py # Brings clicked window to front if self.config.bring_front_click: self.conn.conn.core.ConfigureWindow( wnd, xcffib.xproto.ConfigWindow.StackMode, [xcffib.xproto.StackMode.Above] ) window = self.windowMap.get(wnd) if window and not window.window.get_property('QTILE_INTERNAL'): self.currentGroup.focus(self.windowMap.get(wnd), False) self.windowMap.get(wnd).focus(False) self.conn.conn.core.AllowEvents(xcffib.xproto.Allow.ReplayPointer, e.time) self.conn.conn.flush() def handle_ButtonPress(self, e): button_code = e.detail state = e.state if self.numlockMask: state = e.state | self.numlockMask k = self.mouseMap.get(button_code) for m in k: if not m or m.modmask & self.validMask != state & self.validMask: logger.info("Ignoring unknown button: %s" % button_code) continue if isinstance(m, Click): for i in m.commands: if i.check(self): if m.focus == "before": self.cmd_focus_by_click(e) status, val = self.server.call( (i.selectors, i.name, i.args, i.kwargs)) if m.focus == "after": self.cmd_focus_by_click(e) if status in (command.ERROR, command.EXCEPTION): logger.error( "Mouse command error %s: %s" % (i.name, val) ) elif isinstance(m, Drag): x = e.event_x y = e.event_y if m.start: i = m.start if m.focus == "before": self.cmd_focus_by_click(e) status, val = self.server.call( (i.selectors, i.name, i.args, i.kwargs)) if status in (command.ERROR, command.EXCEPTION): logger.error( "Mouse command error %s: %s" % (i.name, val) ) continue else: val = (0, 0) if m.focus == "after": self.cmd_focus_by_click(e) self._drag = (x, y, val[0], val[1], m.commands) self.root.grab_pointer( True, xcbq.ButtonMotionMask | xcbq.AllButtonsMask | xcbq.ButtonReleaseMask, xcffib.xproto.GrabMode.Async, xcffib.xproto.GrabMode.Async, ) def handle_ButtonRelease(self, e): button_code = e.detail state = e.state & ~xcbq.AllButtonsMask if self.numlockMask: state = state | self.numlockMask k = self.mouseMap.get(button_code) for m in k: if not m: logger.info( "Ignoring unknown button release: %s" % button_code ) continue if isinstance(m, Drag): self._drag = None self.root.ungrab_pointer() def handle_MotionNotify(self, e): if self._drag is None: return ox, oy, rx, ry, cmd = self._drag dx = e.event_x - ox dy = e.event_y - oy if dx or dy: for i in cmd: if i.check(self): status, val = self.server.call(( i.selectors, i.name, i.args + (rx + dx, ry + dy, e.event_x, e.event_y), i.kwargs )) if status in (command.ERROR, command.EXCEPTION): logger.error( "Mouse command error %s: %s" % (i.name, val) ) def handle_ConfigureNotify(self, e): """Handle xrandr events""" screen = self.currentScreen if e.window == self.root.wid and \ e.width != screen.width and \ e.height != screen.height: screen.resize(0, 0, e.width, e.height) def handle_ConfigureRequest(self, e): # It's not managed, or not mapped, so we just obey it. cw = xcffib.xproto.ConfigWindow args = {} if e.value_mask & cw.X: args["x"] = max(e.x, 0) if e.value_mask & cw.Y: args["y"] = max(e.y, 0) if e.value_mask & cw.Height: args["height"] = max(e.height, 0) if e.value_mask & cw.Width: args["width"] = max(e.width, 0) if e.value_mask & cw.BorderWidth: args["borderwidth"] = max(e.border_width, 0) w = xcbq.Window(self.conn, e.window) w.configure(**args) def handle_MappingNotify(self, e): self.conn.refresh_keymap() if e.request == xcffib.xproto.Mapping.Keyboard: self.grabKeys() def handle_MapRequest(self, e): w = xcbq.Window(self.conn, e.window) c = self.manage(w) if c and (not c.group or not c.group.screen): return w.map() def handle_DestroyNotify(self, e): self.unmanage(e.window) def handle_UnmapNotify(self, e): if e.event != self.root.wid: c = self.windowMap.get(e.window) if c and getattr(c, "group", None): try: c.window.unmap() c.state = window.WithdrawnState except xcffib.xproto.WindowError: # This means that the window has probably been destroyed, # but we haven't yet seen the DestroyNotify (it is likely # next in the queue). So, we just let these errors pass # since the window is dead. pass self.unmanage(e.window) def handle_ScreenChangeNotify(self, e): hook.fire("screen_change", self, e) def toScreen(self, n, warp=True): """Have Qtile move to screen and put focus there""" if n >= len(self.screens): return old = self.currentScreen self.currentScreen = self.screens[n] if old != self.currentScreen: hook.fire("current_screen_change") old.group.layoutAll() self.currentGroup.focus(self.currentWindow, warp) def moveToGroup(self, group): """Create a group if it doesn't exist and move a windows there""" if self.currentWindow and group: self.addGroup(group) self.currentWindow.togroup(group) def _items(self, name): if name == "group": return True, list(self.groupMap.keys()) elif name == "layout": return True, list(range(len(self.currentGroup.layouts))) elif name == "widget": return False, list(self.widgetMap.keys()) elif name == "bar": return False, [x.position for x in self.currentScreen.gaps] elif name == "window": return True, self.listWID() elif name == "screen": return True, list(range(len(self.screens))) def _select(self, name, sel): if name == "group": if sel is None: return self.currentGroup else: return self.groupMap.get(sel) elif name == "layout": if sel is None: return self.currentGroup.layout else: return utils.lget(self.currentGroup.layouts, sel) elif name == "widget": return self.widgetMap.get(sel) elif name == "bar": return getattr(self.currentScreen, sel) elif name == "window": if sel is None: return self.currentWindow else: return self.clientFromWID(sel) elif name == "screen": if sel is None: return self.currentScreen else: return utils.lget(self.screens, sel) def listWID(self): return [i.window.wid for i in self.windowMap.values()] def clientFromWID(self, wid): for i in self.windowMap.values(): if i.window.wid == wid: return i return None def call_soon(self, func, *args): """ A wrapper for the event loop's call_soon which also flushes the X event queue to the server after func is called. """ def f(): func(*args) self.conn.flush() return self._eventloop.call_soon(f) def call_soon_threadsafe(self, func, *args): """ Another event loop proxy, see `call_soon`. """ def f(): func(*args) self.conn.flush() return self._eventloop.call_soon_threadsafe(f) def call_later(self, delay, func, *args): """ Another event loop proxy, see `call_soon`. """ def f(): func(*args) self.conn.flush() return self._eventloop.call_later(delay, f) def run_in_executor(self, func, *args): """ A wrapper for running a function in the event loop's default executor. """ return self._eventloop.run_in_executor(None, func, *args) def cmd_debug(self): """Set log level to DEBUG""" logger.setLevel(logging.DEBUG) logger.debug('Switching to DEBUG threshold') def cmd_info(self): """Set log level to INFO""" logger.setLevel(logging.INFO) logger.info('Switching to INFO threshold') def cmd_warning(self): """Set log level to WARNING""" logger.setLevel(logging.WARNING) logger.warning('Switching to WARNING threshold') def cmd_error(self): """Set log level to ERROR""" logger.setLevel(logging.ERROR) logger.error('Switching to ERROR threshold') def cmd_critical(self): """Set log level to CRITICAL""" logger.setLevel(logging.CRITICAL) logger.critical('Switching to CRITICAL threshold') def cmd_pause(self): """Drops into pdb""" import pdb pdb.set_trace() def cmd_groups(self): """Return a dictionary containing information for all groups Examples ======== groups() """ return {i.name: i.info() for i in self.groups} def cmd_get_info(self): """Prints info for all groups""" warnings.warn("The `get_info` command is deprecated, use `groups`", DeprecationWarning) return self.cmd_groups() def cmd_display_kb(self, *args): """Display table of key bindings""" class FormatTable(object): def __init__(self): self.max_col_size = [] self.rows = [] def add(self, row): n = len(row) - len(self.max_col_size) if n > 0: self.max_col_size += [0] * n for i, f in enumerate(row): if len(f) > self.max_col_size[i]: self.max_col_size[i] = len(f) self.rows.append(row) def getformat(self): return " ".join((["%-{0:d}s".format(max_col_size + 2) for max_col_size in self.max_col_size])) + "\n", len(self.max_col_size) def expandlist(self, list, n): if not list: return ["-" * max_col_size for max_col_size in self.max_col_size] n -= len(list) if n > 0: list += [""] * n return list def __str__(self): format, n = self.getformat() return "".join([format % tuple(self.expandlist(row, n)) for row in self.rows]) result = FormatTable() result.add(["KeySym", "Mod", "Command", "Desc"]) result.add([]) rows = [] for (ks, kmm), k in self.keyMap.items(): if not k.commands: continue name = ", ".join(xcbq.rkeysyms.get(ks, ("", ))) modifiers = ", ".join(utils.translate_modifiers(kmm)) allargs = ", ".join([repr(value) for value in k.commands[0].args] + ["%s = %s" % (keyword, repr(value)) for keyword, value in k.commands[0].kwargs.items()]) rows.append((name, str(modifiers), "{0:s}({1:s})".format(k.commands[0].name, allargs), k.desc)) rows.sort() for row in rows: result.add(row) return str(result) def cmd_list_widgets(self): """List of all addressible widget names""" return list(self.widgetMap.keys()) def cmd_to_layout_index(self, index, group=None): """Switch to the layout with the given index in self.layouts. Parameters ========== index : Index of the layout in the list of layouts. group : Group name. If not specified, the current group is assumed. """ if group: group = self.groupMap.get(group) else: group = self.currentGroup group.toLayoutIndex(index) def cmd_next_layout(self, group=None): """Switch to the next layout. Parameters ========== group : Group name. If not specified, the current group is assumed """ if group: group = self.groupMap.get(group) else: group = self.currentGroup group.nextLayout() def cmd_prev_layout(self, group=None): """Switch to the previous layout. Parameters ========== group : Group name. If not specified, the current group is assumed """ if group: group = self.groupMap.get(group) else: group = self.currentGroup group.prevLayout() def cmd_screens(self): """Return a list of dictionaries providing information on all screens""" lst = [dict( index=i.index, group=i.group.name if i.group is not None else None, x=i.x, y=i.y, width=i.width, height=i.height, gaps=dict( top=i.top.geometry() if i.top else None, bottom=i.bottom.geometry() if i.bottom else None, left=i.left.geometry() if i.left else None, right=i.right.geometry() if i.right else None, ) ) for i in self.screens] return lst def cmd_simulate_keypress(self, modifiers, key): """Simulates a keypress on the focused window. Parameters ========== modifiers : A list of modifier specification strings. Modifiers can be one of "shift", "lock", "control" and "mod1" - "mod5". key : Key specification. Examples ======== simulate_keypress(["control", "mod2"], "k") """ # FIXME: This needs to be done with sendevent, once we have that fixed. keysym = xcbq.keysyms.get(key) if keysym is None: raise command.CommandError(u"Unknown key: {0:s}".format(key)) keycode = self.conn.first_sym_to_code[keysym] class DummyEv(object): pass d = DummyEv() d.detail = keycode try: d.state = utils.translate_masks(modifiers) except KeyError as v: return v.args[0] self.handle_KeyPress(d) def cmd_execute(self, cmd, args): """Executes the specified command, replacing the current process""" self.stop() os.execv(cmd, args) def cmd_restart(self): """Restart qtile using the execute command""" argv = [sys.executable] + sys.argv if '--no-spawn' not in argv: argv.append('--no-spawn') buf = io.BytesIO() try: pickle.dump(QtileState(self), buf, protocol=0) except: logger.error("Unable to pickle qtile state") argv = [s for s in argv if not s.startswith('--with-state')] argv.append('--with-state=' + buf.getvalue().decode()) self.cmd_execute(sys.executable, argv) def cmd_spawn(self, cmd): """Run cmd in a shell. cmd may be a string, which is parsed by shlex.split, or a list (similar to subprocess.Popen). Examples ======== spawn("firefox") spawn(["xterm", "-T", "Temporary terminal"]) """ if isinstance(cmd, six.string_types): args = shlex.split(cmd) else: args = list(cmd) r, w = os.pipe() pid = os.fork() if pid < 0: os.close(r) os.close(w) return pid if pid == 0: os.close(r) # close qtile's stdin, stdout, stderr so the called process doesn't # pollute our xsession-errors. os.close(0) os.close(1) os.close(2) pid2 = os.fork() if pid2 == 0: os.close(w) # Open /dev/null as stdin, stdout, stderr try: fd = os.open(os.devnull, os.O_RDWR) except OSError: # This shouldn't happen, catch it just in case pass else: # For Python >=3.4, need to set file descriptor to inheritable try: os.set_inheritable(fd, True) except AttributeError: pass # Again, this shouldn't happen, but we should just check if fd > 0: os.dup2(fd, 0) os.dup2(fd, 1) os.dup2(fd, 2) try: os.execvp(args[0], args) except OSError as e: logger.error("failed spawn: \"{0}\"\n{1}".format(cmd, e)) os._exit(1) else: # Here it doesn't matter if fork failed or not, we just write # its return code and exit. os.write(w, str(pid2).encode()) os.close(w) # sys.exit raises SystemExit, which will then be caught by our # top level catchall and we'll end up with two qtiles; os._exit # actually calls exit. os._exit(0) else: os.close(w) os.waitpid(pid, 0) # 1024 bytes should be enough for any pid. :) pid = os.read(r, 1024) os.close(r) return int(pid) def cmd_status(self): """Return "OK" if Qtile is running""" return "OK" def cmd_sync(self): """Sync the X display. Should only be used for development""" self.conn.flush() def cmd_to_screen(self, n): """Warp focus to screen n, where n is a 0-based screen number Examples ======== to_screen(0) """ return self.toScreen(n) def cmd_next_screen(self): """Move to next screen""" return self.toScreen( (self.screens.index(self.currentScreen) + 1) % len(self.screens) ) def cmd_prev_screen(self): """Move to the previous screen""" return self.toScreen( (self.screens.index(self.currentScreen) - 1) % len(self.screens) ) def cmd_windows(self): """Return info for each client window""" return [ i.info() for i in self.windowMap.values() if not isinstance(i, window.Internal) ] def cmd_internal_windows(self): """Return info for each internal window (bars, for example)""" return [ i.info() for i in self.windowMap.values() if isinstance(i, window.Internal) ] def cmd_qtile_info(self): """Returns a dictionary of info on the Qtile instance""" return dict(socketname=self.fname) def cmd_shutdown(self): """Quit Qtile""" self.stop() def cmd_switch_groups(self, groupa, groupb): """Switch position of groupa to groupb""" if groupa not in self.groupMap or groupb not in self.groupMap: return indexa = self.groups.index(self.groupMap[groupa]) indexb = self.groups.index(self.groupMap[groupb]) self.groups[indexa], self.groups[indexb] = \ self.groups[indexb], self.groups[indexa] hook.fire("setgroup") # update window _NET_WM_DESKTOP for group in (self.groups[indexa], self.groups[indexb]): for w in group.windows: w.group = group def find_window(self, wid): window = self.windowMap.get(wid) if window: if not window.group.screen: self.currentScreen.setGroup(window.group) window.group.focus(window, False) def cmd_findwindow(self, prompt="window", widget="prompt"): """Launch prompt widget to find a window of the given name Parameters ========== prompt : Text with which to prompt user (default: "window") widget : Name of the prompt widget (default: "prompt") """ mb = self.widgetMap.get(widget) if not mb: logger.error("No widget named '{0:s}' present.".format(widget)) return mb.startInput( prompt, self.find_window, "window", strict_completer=True ) def cmd_next_urgent(self): """Focus next window with urgent hint""" try: nxt = [w for w in self.windowMap.values() if w.urgent][0] nxt.group.cmd_toscreen() nxt.group.focus(nxt) except IndexError: pass # no window had urgent set def cmd_togroup(self, prompt="group", widget="prompt"): """Launch prompt widget to move current window to a given group Parameters ========== prompt : Text with which to prompt user (default: "group") widget : Name of the prompt widget (default: "prompt") """ if not self.currentWindow: logger.warning("No window to move") return mb = self.widgetMap.get(widget) if not mb: logger.error("No widget named '{0:s}' present.".format(widget)) return mb.startInput(prompt, self.moveToGroup, "group", strict_completer=True) def cmd_switchgroup(self, prompt="group", widget="prompt"): """Launch prompt widget to switch to a given group to the current screen Parameters ========== prompt : Text with which to prompt user (default: "group") widget : Name of the prompt widget (default: "prompt") """ def f(group): if group: try: self.groupMap[group].cmd_toscreen() except KeyError: logger.info(u"No group named '{0:s}' present.".format(group)) mb = self.widgetMap.get(widget) if not mb: logger.warning("No widget named '{0:s}' present.".format(widget)) return mb.startInput(prompt, f, "group", strict_completer=True) def cmd_spawncmd(self, prompt="spawn", widget="prompt", command="%s", complete="cmd"): """Spawn a command using a prompt widget, with tab-completion. Parameters ========== prompt : Text with which to prompt user (default: "spawn: "). widget : Name of the prompt widget (default: "prompt"). command : command template (default: "%s"). complete : Tab completion function (default: "cmd") """ def f(args): if args: self.cmd_spawn(command % args) try: mb = self.widgetMap[widget] mb.startInput(prompt, f, complete) except KeyError: logger.error("No widget named '{0:s}' present.".format(widget)) def cmd_qtilecmd(self, prompt="command", widget="prompt", messenger="xmessage"): """ Execute a Qtile command using the client syntax Tab completeion aids navigation of the command tree Parameters ========== prompt : Text to display at the prompt (default: "command: ") widget : Name of the prompt widget (default: "prompt") messenger : Command to display output, set this to None to disable (default: "xmessage") """ def f(cmd): if cmd: # c here is used in eval() below c = command.CommandRoot(self) # noqa try: cmd_arg = str(cmd).split(' ') except AttributeError: return cmd_len = len(cmd_arg) if cmd_len == 0: logger.info('No command entered.') return try: result = eval(u'c.{0:s}'.format(cmd)) except ( command.CommandError, command.CommandException, AttributeError) as err: logger.error(err) result = None if result is not None: from pprint import pformat message = pformat(result) if messenger: self.cmd_spawn('{0:s} "{1:s}"'.format(messenger, message)) logger.info(result) mb = self.widgetMap[widget] if not mb: logger.error("No widget named {0:s} present.".format(widget)) return mb.startInput(prompt, f, "qshell") def cmd_addgroup(self, group): """Add a group with the given name""" return self.addGroup(group) def cmd_delgroup(self, group): """Delete a group with the given name""" return self.delGroup(group) def cmd_add_rule(self, match_args, rule_args, min_priorty=False): """Add a dgroup rule, returns rule_id needed to remove it Parameters ========== match_args : config.Match arguments rule_args : config.Rule arguments min_priorty : If the rule is added with minimum prioriry (last) (default: False) """ if not self.dgroups: logger.warning('No dgroups created') return match = Match(**match_args) rule = Rule(match, **rule_args) return self.dgroups.add_rule(rule, min_priorty) def cmd_remove_rule(self, rule_id): """Remove a dgroup rule by rule_id""" self.dgroups.remove_rule(rule_id) def cmd_run_external(self, full_path): """Run external Python script""" def format_error(path, e): s = """Can't call "main" from "{path}"\n\t{err_name}: {err}""" return s.format(path=path, err_name=e.__class__.__name__, err=e) module_name = os.path.splitext(os.path.basename(full_path))[0] dir_path = os.path.dirname(full_path) err_str = "" local_stdout = io.BytesIO() old_stdout = sys.stdout sys.stdout = local_stdout sys.exc_clear() try: module = _import_module(module_name, dir_path) module.main(self) except ImportError as e: err_str += format_error(full_path, e) except: (exc_type, exc_value, exc_traceback) = sys.exc_info() err_str += traceback.format_exc() err_str += format_error(full_path, exc_type(exc_value)) finally: sys.exc_clear() sys.stdout = old_stdout local_stdout.close() return local_stdout.getvalue() + err_str def cmd_hide_show_bar(self, position="all"): """Toggle visibility of a given bar Parameters ========== position : one of: "top", "bottom", "left", "right", or "all" (default: "all") """ if position in ["top", "bottom", "left", "right"]: bar = getattr(self.currentScreen, position) if bar: bar.show(not bar.is_show()) self.currentGroup.layoutAll() else: logger.warning( "Not found bar in position '%s' for hide/show." % position) elif position == "all": screen = self.currentScreen is_show = None for bar in [screen.left, screen.right, screen.top, screen.bottom]: if bar: if is_show is None: is_show = not bar.is_show() bar.show(is_show) if is_show is not None: self.currentGroup.layoutAll() else: logger.warning("Not found bar for hide/show.") else: logger.error("Invalid position value:{0:s}".format(position)) def cmd_get_state(self): """Get pickled state for restarting qtile""" buf = io.BytesIO() pickle.dump(QtileState(self), buf, protocol=0) state = buf.getvalue().decode() logger.info('State = ') logger.info(''.join(state.split('\n'))) return state def cmd_tracemalloc_toggle(self): """Toggle tracemalloc status Running tracemalloc is required for qtile-top """ if not tracemalloc.is_tracing(): tracemalloc.start() else: tracemalloc.stop() def cmd_tracemalloc_dump(self): """Dump tracemalloc snapshot""" if not tracemalloc: logger.warning('No tracemalloc module') raise command.CommandError("No tracemalloc module") if not tracemalloc.is_tracing(): return [False, "Trace not started"] cache_directory = get_cache_dir() malloc_dump = os.path.join(cache_directory, "qtile_tracemalloc.dump") tracemalloc.take_snapshot().dump(malloc_dump) return [True, malloc_dump] def cmd_run_extention(self, cls): """Extentions should run from command run()""" c = cls(self) c.run() qtile-0.10.7/libqtile/notify.py000066400000000000000000000100441305063162100164000ustar00rootroot00000000000000# Copyright (c) 2010 dequis # Copyright (c) 2011 Florian Mounier # Copyright (c) 2011 Mounier Florian # Copyright (c) 2013 Mickael FALCK # Copyright (c) 2013 Tao Sauvage # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """ If dbus is available, this module implements a org.freedesktop.Notifications service. """ from .log_utils import logger try: import dbus from dbus import service from dbus.mainloop.glib import DBusGMainLoop except ImportError: dbus = None BUS_NAME = 'org.freedesktop.Notifications' SERVICE_PATH = '/org/freedesktop/Notifications' if dbus: class NotificationService(service.Object): def __init__(self, manager, loop): bus_name = service.BusName(BUS_NAME, bus=dbus.SessionBus(mainloop=loop)) service.Object.__init__(self, bus_name, SERVICE_PATH) self.manager = manager @service.method(BUS_NAME, in_signature='', out_signature='as') def GetCapabilities(self): return ('body') @service.method( BUS_NAME, in_signature='susssasa{sv}i', out_signature='u' ) def Notify(self, app_name, replaces_id, app_icon, summary, body, actions, hints, timeout): notif = Notification(summary, body, timeout, hints) return self.manager.add(notif) @service.method(BUS_NAME, in_signature='u', out_signature='') def CloseNotification(self, id): pass @service.signal(BUS_NAME, signature='uu') def NotificationClosed(self, id_in, reason_in): pass @service.method(BUS_NAME, in_signature='', out_signature='ssss') def GetServerInformation(self): return ("qtile-notify-daemon", "qtile", "1.0", "1") class Notification(object): def __init__(self, summary, body='', timeout=-1, hints=None): self.summary = summary self.hints = hints or {} self.body = body self.timeout = timeout class NotificationManager(object): def __init__(self): self.notifications = [] self.callbacks = [] self._service = None @property def service(self): if dbus and self._service is None: try: dbus_loop = DBusGMainLoop() self._service = NotificationService(self, dbus_loop) except Exception: logger.exception('Dbus connection failed') self._service = None return self._service def register(self, callback): if not self.service: logger.warning( 'Registering %s without any dbus connection existing', callback.__name__, ) self.callbacks.append(callback) def add(self, notif): self.notifications.append(notif) notif.id = len(self.notifications) for callback in self.callbacks: callback(notif) return len(self.notifications) def show(self, *args, **kwargs): notif = Notification(*args, **kwargs) return (notif, self.add(notif)) notifier = NotificationManager() qtile-0.10.7/libqtile/pangocffi.py000066400000000000000000000155271305063162100170370ustar00rootroot00000000000000# Copyright (c) 2014-2015 Sean Vig # Copyright (c) 2014 roger # Copyright (c) 2014 Tycho Andersen # Copyright (c) 2015 Craig Barnes # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # This module is kind of a hack; you've been warned :-). Some upstream work # needs to happen in order to avoid doing this, though. # # The problem is that we want to use pango to draw stuff. We need to create a # cairo surface, in particular an XCB surface. Since we're using xcffib as the # XCB binding and there is no portable way to go from cffi's PyObject* cdata # wrappers to the wrapped type [1], we can't add support to pycairo for XCB # surfaces via xcffib. # # A similar problem exists one layer of indirection down with cairocffi -- # python's pangocairo is almost all C, and only works by including pycairo's # headers and accessing members of structs only available in C, and not in # python. Since cairocffi is pure python and also cffi based, we cannot extract # the raw pointer to pass to the existing pangocairo bindings. # # The solution here is to implement a tiny pangocffi for the small set of pango # functions we call. We're doing it directly here because we can, but it would # not be difficult to use more upstream libraries (e.g. cairocffi and some # pangocairocffi when it exists). This also allows us to drop pygtk entirely, # since we are doing our own pango binding. # # [1]: https://groups.google.com/forum/#!topic/python-cffi/SPND0rRmazA # # This is not intended to be a complete cffi-based pango binding. import six try: from libqtile._ffi_pango import ffi except ImportError: # PyPy < 2.6 (cffi < 1) compatibility import cffi if cffi.__version_info__[0] == 0: from libqtile.ffi_build import pango_ffi as ffi else: raise ImportError("No module named libqtile._ffi_pango, be sure to run `python ./libqtile/ffi_build.py`") gobject = ffi.dlopen('libgobject-2.0.so.0') pango = ffi.dlopen('libpango-1.0.so.0') pangocairo = ffi.dlopen('libpangocairo-1.0.so.0') def CairoContext(cairo_t): def create_layout(): return PangoLayout(cairo_t._pointer) cairo_t.create_layout = create_layout def show_layout(layout): pangocairo.pango_cairo_show_layout(cairo_t._pointer, layout._pointer) cairo_t.show_layout = show_layout return cairo_t ALIGN_CENTER = pango.PANGO_ALIGN_CENTER ELLIPSIZE_END = pango.PANGO_ELLIPSIZE_END units_from_double = pango.pango_units_from_double def _const_char_to_py_str(cc): return ''.join(ffi.buffer(cc, len(cc))) class PangoLayout(object): def __init__(self, cairo_t): self._cairo_t = cairo_t self._pointer = pangocairo.pango_cairo_create_layout(cairo_t) def free(p): p = ffi.cast("gpointer", p) gobject.g_object_unref(p) self._pointer = ffi.gc(self._pointer, free) def finalize(self): self._desc = None self._pointer = None self._cairo_t = None def set_font_description(self, desc): # save a pointer so it doesn't get GC'd out from under us self._desc = desc pango.pango_layout_set_font_description(self._pointer, desc._pointer) def get_font_description(self): descr = pango.pango_layout_get_font_description(self._pointer) return FontDescription(descr) def set_alignment(self, alignment): pango.pango_layout_set_alignment(self._pointer, alignment) def set_attributes(self, attrs): pango.pango_layout_set_attributes(self._pointer, attrs) def set_text(self, text): text = text.encode('utf-8') pango.pango_layout_set_text(self._pointer, text, -1) def get_text(self): ret = pango.pango_layout_get_text(self._pointer) return _const_char_to_py_str(ret) def set_ellipsize(self, ellipzize): pango.pango_layout_set_ellipsize(self._pointer, ellipzize) def get_ellipsize(self): return pango.pango_layout_get_ellipsize(self._pointer) def get_pixel_size(self): width = ffi.new("int[1]") height = ffi.new("int[1]") pango.pango_layout_get_pixel_size(self._pointer, width, height) return width[0], height[0] def set_width(self, width): pango.pango_layout_set_width(self._pointer, width) class FontDescription(object): def __init__(self, pointer=None): if pointer is None: self._pointer = pango.pango_font_description_new() self._pointer = ffi.gc(self._pointer, pango.pango_font_description_free) else: self._pointer = pointer @classmethod def from_string(cls, string): pointer = pango.pango_font_description_from_string(string.encode()) pointer = ffi.gc(pointer, pango.pango_font_description_free) return cls(pointer) def set_family(self, family): pango.pango_font_description_set_family(self._pointer, family.encode()) def get_family(self): ret = pango.pango_font_description_get_family(self._pointer) return _const_char_to_py_str(ret) def set_absolute_size(self, size): pango.pango_font_description_set_absolute_size(self._pointer, size) def set_size(self, size): pango.pango_font_description_set_size(self._pointer, size) def get_size(self, size): return pango.pango_font_description_get_size(self._pointer, size) def parse_markup(value, accel_marker=0): attr_list = ffi.new("PangoAttrList**") text = ffi.new("char**") error = ffi.new("GError**") if six.PY3: value = value.encode() ret = pango.pango_parse_markup(value, -1, accel_marker, attr_list, text, ffi.NULL, error) if ret == 0: raise Exception("parse_markup() failed for %s" % value) return attr_list[0], ffi.string(text[0]), six.unichr(accel_marker) def markup_escape_text(text): ret = gobject.g_markup_escape_text(text.encode('utf-8'), -1) if six.PY3: return ffi.string(ret).decode() return ffi.string(ret) qtile-0.10.7/libqtile/resources/000077500000000000000000000000001305063162100165315ustar00rootroot00000000000000qtile-0.10.7/libqtile/resources/__init__.py000066400000000000000000000000001305063162100206300ustar00rootroot00000000000000qtile-0.10.7/libqtile/resources/battery-icons/000077500000000000000000000000001305063162100213145ustar00rootroot00000000000000qtile-0.10.7/libqtile/resources/battery-icons/battery-caution-charging.png000066400000000000000000000024431305063162100267170ustar00rootroot00000000000000PNG  IHDRw=sBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org< tEXtTitleBatteryUD?PtEXtAuthorLapo Calamandreiߑ*dIDATH[lUgfV텭@iIaP* ,Z"!<@E}   hbcEB6HV6Ks=>vAD dΙ/wϜ#@xP2h3 ڜR]ow>(9L L}<'O7oE"'4`=\׵EBU48u{ 8c{']uUu\׵|Ɇq1R`Yֽ l_yWrH ٶm<)i¶M\8 s IXy뺮iwcY~s@>x(_UcSG-*Vɝc&<PTUeY%KzW;y=Url`$;i=NLY O,YUU0M77 |m嬾OVc7Hz&5۶=)ii0MWoUNӕy+?4Eu-_:nSŋ@Zi[%]uHRȷ`pg^ϓJKנ:`H!S9KhmAʰuj*CHr;!D.F/_.uyٙEEE1 Xa{{aqaԄ~ H455 έ+){ޣ7Ѩ@" 7v0A&B@RBڐi~?0 q!#RJc".;.9ΑnWU}bb&`J)ʹX ٖ?En 9/RRtdIENDB`qtile-0.10.7/libqtile/resources/battery-icons/battery-caution.png000066400000000000000000000021621305063162100251350ustar00rootroot00000000000000PNG  IHDRw=sBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org< tEXtTitleBatteryUD?PtEXtAuthorLapo Calamandreiߑ*IDATH]h\Egfu7! XCh5Ҡ>>XhIHJbQQ ~D| *E !/&AD 4&ƻ3ΌɦIu?g9dō4qu7Nc1ze/.(JD800"6 Zlih~~>fRZ)=3yNDV ?]n5ِjڭm=X^^$BwGo8kKKK?...69竫S=22~X\3:`!".0 ՞0 1VkpqHJ #" c( "& j. Eκ8|SJk cZ3!R03HnfPN"LqI)&''tɉ9RN&@J(:@sT*2c- YogoM= ] /O掿w> Zi TVUI@-!ÉFP9^ADb>ouww;Ƙsssb>(X, ܇iV#YX gZ[U/hLU13Mx(β TUSJVe}3õgXD86%f~p}}=Ǯ,K1n۽`HD-3+5ZBXL&͢(03ѿumBlYJ4?H*< v{ucx, " 'D9\5hP_fn K|έK,`!ߺi}_U:KO$|G I'I4f {=ُ՝;T*'8УɵZmܒ*Z6y2ZDr@ RJ@)334+0=Z H)0FqB}o@WRhV@DRJ?>x'|ޱH7AJ)Ȼ>X!}?S1uv ])bd!*>.>];38~lCoP,} =` 3A!+J/AmE??@qɩs3rzu[VWBYNQ#zC$\7Ds0sp-0˟;Y(D/dk$0 1tmXjy Z3R09^6YaZ5FcѠeYFF!099<$ 0Q }FݓG:[-` 5NcZm4ц2ăԽc]/;FՕsR9nvɬeT 0V`5w9jD[f/ mG"iT64qn=*^h2K5^{D,&P竢q6̢lɗdбÝz+~>HIENDB`qtile-0.10.7/libqtile/resources/battery-icons/battery-full-charging.png000066400000000000000000000023421305063162100262150ustar00rootroot00000000000000PNG  IHDRw=sBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org< tEXtTitleBatteryUD?PtEXtAuthorLapo Calamandreiߑ*#IDATHMlTUwf:SLVj@Rc$1.q! 1$,P4~ĸ 1H ȇ"„v25@5)-ͻt,eD҄9ォD[?x˻'|>d |TRjR -ohMNAD I$#eeYwklLQ8̰ I 5r"H PWo*0;zVtV{iW"yb[6Q^ƒ\>CM[qe#Z;µ&[;{pk//+ybըãXdRc;ǃ'o쁸"J),t:m_q6W=fPeOo; vK}^),qGOQ^A.s˻,^;BA*]EXE!1L@j7FfG\"\O>;@*&@t;8"f su#,vɷ|y;PCoF;?7ܽAi2 xmm{(zfZXk3p9U%Fᜫ*äEdqџ|Xk Z?C6 2Ѐ*"c#08pr&>cUAoeY+W%PESTụ1 iVK "-`1˲Υ[E1X+m`qR"Q[ӹz\<{ "cU-yCu-AKfUe`QV1 FUTE_[~dHpz iIENDB`qtile-0.10.7/libqtile/resources/battery-icons/battery-good-charging.png000066400000000000000000000024031305063162100262010ustar00rootroot00000000000000PNG  IHDRw=sBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org< tEXtTitleBatteryUD?PtEXtAuthorLapo Calamandreiߑ*DIDATH[lTU}f8)vJ[- RI0*Bچ 1FR_ !#JFc DEB2-A:-j/JPڙɴÙs>b)#&d=W_Yg )%sgbOGnP}T !DbʖkjB2' `]$CRuhL.xqF:[T,Z|ݗ/Spf/-+SUu=O&yJ?P*zÆ dfu%mTE\.URU˲5%,tGJ)-vm7N5\].,2{gسl=skcaxDmFizJ=xRT/v~wof&O>գlӒI!%ucH^`J]71sBߋ* hoKf,-%-SaJ=xUA~l\CZ*c}$i9EN|迋0 v$]׭ήxӻo>vDj,{>¡q$ "G D"]_寺~9w*?sS"y k0ʨ@um)<4<9.Y\^iW.6 bf{ d5 @8܊D"NSS8HRJ;r\Ck-KhYp=#c*-Zm]f_RWE(HExus7d*Ձd @JYP8 eKJY^RJ 2!. R2`rP!bfh~B/j͑ ,+Ձf0,4-̎TpuDFQ4VoD2s" 譭n_8899[{gvu(Q"3sy=B<ϫAfYf]M0CG@`f=ooagα,IENDB`qtile-0.10.7/libqtile/resources/battery-icons/battery-low-charging.png000066400000000000000000000024341305063162100260560ustar00rootroot00000000000000PNG  IHDRשsBITO pHYs B(xtEXtSoftwarewww.inkscape.org< tEXtTitleBatteryUD?PtEXtAuthorLapo Calamandreiߑ*:PLTE$$${{QQQUUU[[[___cccfffiii```bbbgggjjjlllppprrrܟߴᵵ06828: ⿿27939;SWYooo|||NSSehh/5727:y{y?l3::PwIqU{Nt7aFmGmJr\^\\_\ceccfd}/46057Gmhop8bDklǎԐէ/57лѫ.466=8poLN%Ex)Ct  ĭQ[ݚL#@@=' "4xPv7,մд_xJkX8Mm#?sPȠ6gᚰFl#mk {"i |Gd2MCCl =@da꭯i93듇O(KHLF4] ȻCIENDB`qtile-0.10.7/libqtile/resources/battery-icons/battery-low.png000066400000000000000000000014141305063162100242730ustar00rootroot00000000000000PNG  IHDRw=sBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org< tEXtTitleBatteryUD?PtEXtAuthorLapo Calamandreiߑ*MIDATHkAǿofF!.D=bq Ev}H)),,DHrŋy3" rf`a>;o$"8<(E޻_SEhf&Dd0tZKˋbc,-/ "%zTX[Ok_d@Rxqfj* n]/4-,,z+i}U&pYiC"al"" @A@9[dYe@TnQنS@, (0$Ql}~WrDr0q|,="E"Z;oF"nj+0JDaMW ƣ(,s̼ `@oX?D#~O0B[~Uo ԜIENDB`qtile-0.10.7/libqtile/resources/battery-icons/battery-missing.png000066400000000000000000000015621305063162100251470ustar00rootroot00000000000000PNG  IHDRw=sBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org< tEXtTitleBatteryUD?PtEXtAuthorLapo Calamandreiߑ*IDATHOQAc@&*3t#QcLt_,NT&?6L'bb$ "B}9@ PJo};s FGGN}ŅK2fgg/777ƺ08 `eesg0TKKKˋG+5tb|||&rdB9mNxZS52sY3V `5yTJY:,Q4%T([#"0T/1b(Zk3#ZkW Ak}e7J)!N>F O~xBSLpE?=YCk \zn:Rʠ&1@o_^x녔į, J}~uuUHB@4p_Ԓex2 qъ!$pH hϲɖ@9`RvN0  BBr"^`\b*&ś!`&XDwCH/!4j&,#0!.M1`"⛃Tv⻷ʅÙ#>y̐|*xJ;'$Lpf` loF|@"oy:$5/Rn}D%=j^x61@aL&d0\]Emy@Nsx"K fS2PV  fX΃"T$;v8l^2ك}IENDB`qtile-0.10.7/libqtile/resources/layout-icons/layout-monadtall.png000066400000000000000000000010021305063162100251440ustar00rootroot00000000000000PNG  IHDR@@iqbKGDC pHYs  tIME 1֑aiTXtCommentCreated with GIMPd.efIDATxK0D(2FQ;eRjDU|x||Q p\_ v-'q0GMTD`D?#E@( >Bd=E@Vx/,Q(l(bMx3?P EPkl$)]{sN=b7֜#|KaU+,V))AV,G~pV+P3| BkW4B8XaKjl$@#`zw-0RɬF<#դ85]n5ȳt-5`-1k@-a> 9e:J0^>;L+L>~+lv8lIENDB`qtile-0.10.7/libqtile/resources/layout-icons/layout-ratiotile.png000066400000000000000000000006311305063162100251740ustar00rootroot00000000000000PNG  IHDR@@iqbKGDC pHYs  tIME 7iTXtCommentCreated with GIMPd.eIDATx @Qܽ.d\ `9YA'ffM`z4#wub$ZE@PyF^aټ])*(_րEX| w޻;RK4@v\o^|x}kH=㱘'/t_ݻ@g(-bO-Wxvdbht?gIENDB`qtile-0.10.7/libqtile/resources/layout-icons/layout-treetab.png000066400000000000000000000010261305063162100246250ustar00rootroot00000000000000PNG  IHDR@@iqbKGDC pHYs  tIME )R7ܝiTXtCommentCreated with GIMPd.ezIDATxZ!6fE-Qac VN@?L09,.mu{Vո ߿ip  6 :FN@o;  4 ":rnzE@RG>`d`t>LgzwHWV/e\!![kO&sFܑS`vo|hu9!i0L_@`t(#D/UH$RG<j-D‰/Fn{뛡""뚗cHH$GWp:+YmDs㡟ɝ*U`R++4K\ EoaCE@>"IENDB`qtile-0.10.7/libqtile/resources/layout-icons/layout-unknown.png000066400000000000000000000015061305063162100247010ustar00rootroot00000000000000PNG  IHDR@@iqbKGD pHYs  tIME .,*~_''Yw N ;`E4EO\T{>*ޢ)VIGh?KK@((&Hk"O`phě'q`m'#䜻YyιV\kD ȹ|>55e*];!`9W(Tۭʿ#>Xk/5b  @ |;,K@ w>FIENDB`qtile-0.10.7/libqtile/resources/layout-icons/layout-verticaltile.png000066400000000000000000000007641305063162100256760ustar00rootroot00000000000000PNG  IHDR@@iqbKGDC pHYs  tIME B NiTXtCommentCreated with GIMPd.eXIDATxZ Ӂ7DAL㣰iB6ݽj9| ff}&OОɦ5<# 5< +wcAA DV *D ~#])W Ѿ1@, 4G_- &kA pμT#$D@agFN+Y+-/H|  sQBJ&ߝ#`рPI C-o[F˶N [ş"ˠTE!5 AR@pl?*#`XdeH䤘FPbAAp4sIENDB`qtile-0.10.7/libqtile/resources/layout-icons/layout-wmii.png000066400000000000000000000007251305063162100241510ustar00rootroot00000000000000PNG  IHDR@@iqbKGDC pHYs  tIME  W|f9iTXtCommentCreated with GIMPd.e9IDATxK0 (YUjL$6Yd߳j vCD#.5x=yǘ4E"}eD-Ezմ/)4 JP%`#g,ɀoȮ%pjoMgCeh+Ғ L+ҒfU>{ 4`h6e+hM(y"), tracemalloc.Filter(False, ""), )) def get_stats(scr, client, group_by='lineno', limit=10, seconds=1.5, force_start=False): (max_y, max_x) = scr.getmaxyx() curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLACK) while True: scr.addstr(0, 0, "Qtile - Top {0:s} lines".format(limit)) scr.addstr(1, 0, '{0:<3s} {1:<40s} {2:<30s} {3:<16s}'.format('#', 'Line', 'Memory', ' ' * (max_x - 71)), curses.A_BOLD | curses.A_REVERSE) snapshot = get_trace(client, force_start) snapshot = filter_snapshot(snapshot) top_stats = snapshot.statistics(group_by) cnt = 1 for index, stat in enumerate(top_stats[:limit], 1): frame = stat.traceback[0] # replace "/path/to/module/file.py" with "module/file.py" filename = os.sep.join(frame.filename.split(os.sep)[-2:]) code = "" line = linecache.getline(frame.filename, frame.lineno).strip() if line: code = line mem = "{0:.1f} KiB".format(stat.size / 1024) filename = "{0:s}:{1:s}".format(filename, frame.lineno) scr.addstr(cnt + 1, 0, '{0:<3s} {1:<40s} {2:<30s}'.format(index, filename, mem)) scr.addstr(cnt + 2, 4, code, curses.color_pair(1)) cnt += 2 other = top_stats[limit:] cnt += 2 if other: size = sum(stat.size for stat in other) other_size = ("{0:s} other: {1:.1f} KiB".format(len(other), size / 1024)) scr.addstr(cnt, 0, other_size, curses.A_BOLD) cnt += 1 total = sum(stat.size for stat in top_stats) total_size = "Total allocated size: {0:.1f} KiB".format(total / 1024) scr.addstr(cnt, 0, total_size, curses.A_BOLD) scr.move(max_y - 2, max_y - 2) scr.refresh() time.sleep(seconds) scr.erase() def raw_stats(client, group_by='lineno', limit=10, force_start=False): snapshot = get_trace(client, force_start) snapshot = filter_snapshot(snapshot) top_stats = snapshot.statistics(group_by) print("Qtile - Top {0:s} lines".format(limit)) for index, stat in enumerate(top_stats[:limit], 1): frame = stat.traceback[0] # replace "/path/to/module/file.py" with "module/file.py" filename = os.sep.join(frame.filename.split(os.sep)[-2:]) print("#{0:s}: {1:s}:{2:s}: {3:.1f} KiB" .format(index, filename, frame.lineno, stat.size / 1024)) line = linecache.getline(frame.filename, frame.lineno).strip() if line: print(' {0:s}'.format(line)) other = top_stats[limit:] if other: size = sum(stat.size for stat in other) print("{0:s} other: {1:.1f} KiB".format(len(other), size / 1024)) total = sum(stat.size for stat in top_stats) print("Total allocated size: {0:.1f} KiB".format(total / 1024)) def main(): opts = parse_args() lines = opts.lines seconds = opts.seconds force_start = opts.force_start client = command.Client(opts.socket) try: if not opts.raw: curses.wrapper(get_stats, client, limit=lines, seconds=seconds, force_start=force_start) else: raw_stats(client, limit=lines, force_start=force_start) except TraceNotStarted: print("tracemalloc not started on qtile, start by setting " "PYTHONTRACEMALLOC=1 before starting qtile") print("or force start tracemalloc now, but you'll lose early traces") exit(1) except TraceCantStart: print("Can't start tracemalloc on qtile, check the logs") except KeyboardInterrupt: exit(-1) qtile-0.10.7/libqtile/sh.py000066400000000000000000000230221305063162100155020ustar00rootroot00000000000000# Copyright (c) 2008, Aldo Cortesi. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """ A command shell for Qtile. """ from __future__ import division, print_function import fcntl import inspect import pprint import re import readline import sys import struct import six import termios from six.moves import input from . import command from . import ipc def terminalWidth(): width = None try: cr = struct.unpack('hh', fcntl.ioctl(0, termios.TIOCGWINSZ, '1234')) width = int(cr[1]) except (IOError, ImportError): pass return width or 80 class QSh(object): """Qtile shell instance""" def __init__(self, client, completekey="tab"): self.clientroot = client self.current = client self.completekey = completekey self.builtins = [i[3:] for i in dir(self) if i.startswith("do_")] self.termwidth = terminalWidth() def _complete(self, buf, arg): if not re.search(r" |\(", buf) or buf.startswith("help "): options = self.builtins + self._commands lst = [i for i in options if i.startswith(arg)] return lst elif buf.startswith("cd ") or buf.startswith("ls "): path = [i for i in arg.split("/") if i] if arg.endswith("/"): last = "" else: last = path[-1] path = path[:-1] node = self._findNode(self.current, *path) options = [str(i) for i in self._ls(node)] lst = [] path = "/".join(path) if path: path += "/" for i in options: if i.startswith(last): lst.append(path + i) return lst def complete(self, arg, state): buf = readline.get_line_buffer() completers = self._complete(buf, arg) if completers and state < len(completers): return completers[state] @property def prompt(self): return "%s> " % self.current.path def columnize(self, lst, update_termwidth=True): if update_termwidth: self.termwidth = terminalWidth() ret = [] if lst: lst = list(map(str, lst)) mx = max(map(len, lst)) cols = self.termwidth // (mx + 2) or 1 # We want `(n-1) * cols + 1 <= len(lst) <= n * cols` to return `n` # If we subtract 1, then do `// cols`, we get `n - 1`, so we can then add 1 rows = (len(lst) - 1) // cols + 1 for i in range(rows): # Because Python array slicing can go beyond the array bounds, # we don't need to be careful with the values here sl = lst[i * cols: (i + 1) * cols] sl = [x + " " * (mx - len(x)) for x in sl] ret.append(" ".join(sl)) return "\n".join(ret) def _inspect(self, obj): """Returns an (attrs, keys) tuple""" if obj.parent and obj.myselector is None: t, itms = obj.parent.items(obj.name) attrs = obj._contains if t else None return (attrs, itms) else: return (obj._contains, []) def _ls(self, obj): attrs, itms = self._inspect(obj) all = [] if attrs: all.extend(attrs) if itms: all.extend(itms) return all @property def _commands(self): try: # calling `.commands()` here triggers `CommandRoot.cmd_commands()` return self.current.commands() except command.CommandError: return [] def _findNode(self, src, *path): """Returns a node, or None if no such node exists""" if not path: return src attrs, itms = self._inspect(src) next = None if path[0] == "..": next = src.parent or src else: for trans in [str, int]: try: tpath = trans(path[0]) except ValueError: continue if attrs and tpath in attrs: next = getattr(src, tpath) elif itms and tpath in itms: next = src[tpath] if next: if path[1:]: return self._findNode(next, *path[1:]) else: return next else: return None def do_cd(self, arg): """Change to another path. Examples ======== cd layout/0 cd ../layout """ next = self._findNode(self.current, *[i for i in arg.split("/") if i]) if next: self.current = next return self.current.path or '/' else: return "No such path." def do_ls(self, arg): """List contained items on a node. Examples ======== > ls > ls ../layout """ l = self._ls(self.current) l = ["%s/" % i for i in l] return self.columnize(l) def do_pwd(self, arg): """Returns the current working location This is the same information as presented in the qshell prompt, but is very useful when running iqshell. Examples ======== > pwd / > cd bar/top bar['top']> pwd bar['top'] """ return self.current.path or '/' def do_help(self, arg): """Give help on commands and builtins When invoked without arguments, provides an overview of all commands. When passed as an argument, also provides a detailed help on a specific command or builtin. Examples ======== > help > help command """ if not arg: lst = [ "help command -- Help for a specific command.", "", "Builtins", "========", self.columnize(self.builtins), ] cmds = self._commands if cmds: lst.extend([ "", "Commands for this object", "========================", self.columnize(cmds), ]) return "\n".join(lst) elif arg in self._commands: return self._call("doc", "(\"%s\")" % arg) elif arg in self.builtins: c = getattr(self, "do_" + arg) return inspect.getdoc(c) else: return "No such command: %s" % arg def do_exit(self, args): """Exit qshell""" sys.exit(0) do_quit = do_exit do_q = do_exit def _call(self, cmd_name, args): cmds = self._commands if cmd_name not in cmds: return "No such command: %s" % cmd_name cmd = getattr(self.current, cmd_name) if args: args = "".join(args) else: args = "()" try: val = eval( "cmd%s" % args, {}, dict(cmd=cmd) ) return val except SyntaxError as v: return "Syntax error in expression: %s" % v.text except command.CommandException as val: return "Command exception: %s\n" % val except ipc.IPCError: # on restart, try to reconnect if cmd_name == 'restart': client = command.Client(self.clientroot.client.fname) self.clientroot = client self.current = client else: raise def process_command(self, line): match = re.search(r"\W", line) if match: cmd = line[:match.start()].strip() args = line[match.start():].strip() else: cmd = line args = '' builtin = getattr(self, "do_" + cmd, None) if builtin: val = builtin(args) else: val = self._call(cmd, args) return val def loop(self): readline.set_completer(self.complete) readline.parse_and_bind(self.completekey + ": complete") readline.set_completer_delims(" ()|") while True: try: line = input(self.prompt) except (EOFError, KeyboardInterrupt): print() return if not line: continue val = self.process_command(line) if isinstance(val, six.string_types): print(val) elif val: pprint.pprint(val) qtile-0.10.7/libqtile/state.py000066400000000000000000000050051305063162100162110ustar00rootroot00000000000000# Copyright (c) 2012, Tycho Andersen. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. class QtileState(object): """Represents the state of the qtile object Primarily used for restoring state across restarts; any additional state which doesn't fit nicely into X atoms can go here. """ def __init__(self, qtile): # Note: window state is saved and restored via _NET_WM_STATE, so # the only thing we need to restore here is the layout and screen # configurations. self.groups = [] self.screens = {} self.current_screen = 0 for group in qtile.groups: self.groups.append((group.name, group.layout.name)) for index, screen in enumerate(qtile.screens): self.screens[index] = screen.group.name if screen == qtile.currentScreen: self.current_screen = index def apply(self, qtile): """ Rearrange the windows in the specified Qtile object according to this QtileState. """ for (group, layout) in self.groups: try: qtile.groupMap[group].layout = layout except KeyError: qtile.addGroup(group, layout) for (screen, group) in self.screens.items(): try: group = qtile.groupMap[group] qtile.screens[screen].setGroup(group) except (KeyError, IndexError): pass # group or screen missing qtile.toScreen(self.current_screen) qtile-0.10.7/libqtile/utils.py000066400000000000000000000160101305063162100162270ustar00rootroot00000000000000# Copyright (c) 2008, Aldo Cortesi. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import functools import os import operator import sys import warnings import six from six.moves import reduce from . import xcbq from .log_utils import logger class QtileError(Exception): pass def lget(o, v): try: return o[v] except (IndexError, TypeError): return None def translate_masks(modifiers): """ Translate a modifier mask specified as a list of strings into an or-ed bit representation. """ masks = [] for i in modifiers: try: masks.append(xcbq.ModMasks[i]) except KeyError: raise KeyError("Unknown modifier: %s" % i) if masks: return reduce(operator.or_, masks) else: return 0 def translate_modifiers(mask): r = [] for k, v in xcbq.ModMasks.items(): if mask & v: r.append(k) return r def shuffleUp(lst): if len(lst) > 1: c = lst[-1] lst.remove(c) lst.insert(0, c) def shuffleDown(lst): if len(lst) > 1: c = lst[0] lst.remove(c) lst.append(c) if sys.version_info < (3, 3): class lru_cache(object): """ A decorator that implements a self-expiring LRU cache for class methods (not functions!). Cache data is tracked as attributes on the object itself. There is therefore a separate cache for each object instance. """ def __init__(self, maxsize=128, typed=False): self.size = maxsize def __call__(self, f): cache_name = "_cached_{0}".format(f.__name__) cache_list_name = "_cachelist_{0}".format(f.__name__) size = self.size @functools.wraps(f) def wrap(self, *args): if not hasattr(self, cache_name): setattr(self, cache_name, {}) setattr(self, cache_list_name, []) cache = getattr(self, cache_name) cache_list = getattr(self, cache_list_name) if args in cache: cache_list.remove(args) cache_list.insert(0, args) return cache[args] else: ret = f(self, *args) cache_list.insert(0, args) cache[args] = ret if len(cache_list) > size: d = cache_list.pop() cache.pop(d) return ret return wrap else: from functools import lru_cache def rgb(x): """ Returns a valid RGBA tuple. Here are some valid specifcations: #ff0000 ff0000 with alpha: ff0000.5 (255, 0, 0) (255, 0, 0, 0.5) """ if isinstance(x, (tuple, list)): if len(x) == 4: alpha = x[3] else: alpha = 1 return (x[0] / 255.0, x[1] / 255.0, x[2] / 255.0, alpha) elif isinstance(x, six.string_types): if x.startswith("#"): x = x[1:] if "." in x: x, alpha = x.split(".") alpha = float("0." + alpha) else: alpha = 1 if len(x) != 6: raise ValueError("RGB specifier must be 6 characters long.") vals = [int(i, 16) for i in (x[0:2], x[2:4], x[4:6])] vals.append(alpha) return rgb(vals) raise ValueError("Invalid RGB specifier.") def hex(x): r, g, b, _ = rgb(x) return '#%02x%02x%02x' % (int(r * 255), int(g * 255), int(b * 255)) def scrub_to_utf8(text): if not text: return u"" elif isinstance(text, six.text_type): return text else: return text.decode("utf-8", "ignore") # WARNINGS class UnixCommandNotFound(Warning): pass def catch_exception_and_warn(warning=Warning, return_on_exception=None, excepts=Exception): """ .. function:: warn_on_exception(func, [warning_class, return_on_failure, excepts]) attempts to call func. catches exception or exception tuple and issues a warning instead. returns value of return_on_failure when the specified exception is raised. :param func: a callable to be wrapped :param warning: the warning class to issue if an exception is raised :param return_on_exception: the default return value of the function if an exception is raised :param excepts: an exception class (or tuple of exception classes) to catch during the execution of func :type excepts: Exception or tuple of Exception classes :type warning: Warning :rtype: a callable """ def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): return_value = return_on_exception try: return_value = func(*args, **kwargs) except excepts as err: logger.warn(err.strerror) warnings.warn(err.strerror, warning) return return_value return wrapper return decorator def get_cache_dir(): """ Returns the cache directory and create if it doesn't exists """ cache_directory = os.path.expandvars('$XDG_CACHE_HOME') if cache_directory == '$XDG_CACHE_HOME': # if variable wasn't set cache_directory = os.path.expanduser("~/.cache") cache_directory = os.path.join(cache_directory, 'qtile') if not os.path.exists(cache_directory): os.makedirs(cache_directory) return cache_directory def describe_attributes(obj, attrs, func=None): """ Helper for __repr__ functions to list attributes with truthy values only (or values that return a truthy value by func) """ if not func: func = lambda x: x # flake8: noqa pairs = [] for attr in attrs: value = getattr(obj, attr, None) if func(value): pairs.append('%s=%s' % (attr, value)) return ', '.join(pairs) qtile-0.10.7/libqtile/widget/000077500000000000000000000000001305063162100160025ustar00rootroot00000000000000qtile-0.10.7/libqtile/widget/__init__.py000066400000000000000000000076461305063162100201300ustar00rootroot00000000000000# Copyright (c) 2014 Rock Neurotiko # Copyright (c) 2014 roger # Copyright (c) 2015 David R. Andersen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import traceback import importlib from libqtile.log_utils import logger def safe_import(module_name, class_name): """ try to import a module, and if it fails because an ImporError it logs on WARNING, and logs the traceback on DEBUG level """ if type(class_name) is list: for name in class_name: safe_import(module_name, name) return package = __package__ # TODO: remove when we really want to drop 3.2 support # python 3.2 don't set __package__ if not package: package = __name__ try: module = importlib.import_module(module_name, package) globals()[class_name] = getattr(module, class_name) except ImportError as error: logger.warning("Unmet dependencies for optional Widget: '%s.%s', %s", module_name, class_name, error) logger.debug("%s", traceback.format_exc()) safe_import(".backlight", "Backlight") safe_import(".battery", ["Battery", "BatteryIcon"]) safe_import(".clock", "Clock") safe_import(".currentlayout", ["CurrentLayout", "CurrentLayoutIcon"]) safe_import(".currentscreen", "CurrentScreen") safe_import(".debuginfo", "DebugInfo") safe_import(".graph", ["CPUGraph", "MemoryGraph", "SwapGraph", "NetGraph", "HDDGraph", "HDDBusyGraph"]) safe_import(".groupbox", ["AGroupBox", "GroupBox"]) safe_import(".maildir", "Maildir") safe_import(".notify", "Notify") safe_import(".prompt", "Prompt") safe_import(".sensors", "ThermalSensor") safe_import(".sep", "Sep") safe_import(".she", "She") safe_import(".spacer", "Spacer") safe_import(".systray", "Systray") safe_import(".textbox", "TextBox") safe_import(".generic_poll_text", ["GenPollText", "GenPollUrl"]) safe_import(".volume", "Volume") safe_import(".windowname", "WindowName") safe_import(".windowtabs", "WindowTabs") safe_import(".keyboardlayout", "KeyboardLayout") safe_import(".df", "DF") safe_import(".image", "Image") safe_import(".gmail_checker", "GmailChecker") safe_import(".clipboard", "Clipboard") safe_import(".countdown", "Countdown") safe_import(".tasklist", "TaskList") safe_import(".pacman", "Pacman") safe_import(".launchbar", "LaunchBar") safe_import(".canto", "Canto") safe_import(".mpriswidget", "Mpris") safe_import(".mpris2widget", "Mpris2") safe_import(".mpdwidget", "Mpd") safe_import(".mpd2widget", "Mpd2") safe_import(".yahoo_weather", "YahooWeather") safe_import(".bitcoin_ticker", "BitcoinTicker") safe_import(".wlan", "Wlan") safe_import(".khal_calendar", "KhalCalendar") safe_import(".imapwidget", "ImapWidget") safe_import(".net", "Net") safe_import(".keyboardkbdd", "KeyboardKbdd") safe_import(".cmus", "Cmus") safe_import(".wallpaper", "Wallpaper") safe_import(".check_updates", "CheckUpdates") safe_import(".moc", "Moc") safe_import(".memory", "Memory") safe_import(".idlerpg", "IdleRPG") qtile-0.10.7/libqtile/widget/backlight.py000066400000000000000000000073121305063162100203070ustar00rootroot00000000000000# Copyright (c) 2012 Tim Neumann # Copyright (c) 2012, 2014 Tycho Andersen # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 Sean Vig # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import os from . import base from libqtile.log_utils import logger BACKLIGHT_DIR = '/sys/class/backlight' class Backlight(base.InLoopPollText): """A simple widget to show the current brightness of a monitor""" filenames = {} orientations = base.ORIENTATION_HORIZONTAL defaults = [ ('backlight_name', 'acpi_video0', 'ACPI name of a backlight device'), ( 'brightness_file', 'brightness', 'Name of file with the ' 'current brightness in /sys/class/backlight/backlight_name' ), ( 'max_brightness_file', 'max_brightness', 'Name of file with the ' 'maximum brightness in /sys/class/backlight/backlight_name' ), ('update_interval', .2, 'The delay in seconds between updates'), ('step', 10, 'Percent of backlight every scroll changed'), ('format', '{percent: 2.0%}', 'Display format') ] def __init__(self, **config): base.InLoopPollText.__init__(self, **config) self.add_defaults(Backlight.defaults) self.future = None def _load_file(self, name): try: path = os.path.join(BACKLIGHT_DIR, self.backlight_name, name) with open(path, 'r') as f: return f.read().strip() except: logger.exception("Failed to read file: %s" % name) raise def _get_info(self): try: info = { 'brightness': float(self._load_file(self.brightness_file)), 'max': float(self._load_file(self.max_brightness_file)), } except: return return info def poll(self): info = self._get_info() if not info: return 'Error' percent = info['brightness'] / info['max'] return self.format.format(percent=percent) def change_backlight(self, value): self.call_process(["xbacklight", "-set", str(value)]) def button_press(self, x, y, button): if self.future and not self.future.done(): return info = self._get_info() if info is False: new = now = 100 else: new = now = info["brightness"] / info["max"] * 100 if button == 5: # down new = max(now - self.step, 0) elif button == 4: # up new = min(now + self.step, 100) if new != now: self.future = self.qtile.run_in_executor(self.change_backlight, new) qtile-0.10.7/libqtile/widget/base.py000066400000000000000000000453121305063162100172730ustar00rootroot00000000000000# Copyright (c) 2008-2010 Aldo Cortesi # Copyright (c) 2011 Florian Mounier # Copyright (c) 2011 Kenji_Takahashi # Copyright (c) 2011 Paul Colomiets # Copyright (c) 2012 roger # Copyright (c) 2012 Craig Barnes # Copyright (c) 2012-2015 Tycho Andersen # Copyright (c) 2013 dequis # Copyright (c) 2013 David R. Andersen # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014-2015 Sean Vig # Copyright (c) 2014 Justin Bronder # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from libqtile.log_utils import logger from .. import command, bar, configurable, drawer, confreader import six import subprocess import threading import warnings # Each widget class must define which bar orientation(s) it supports by setting # these bits in an 'orientations' class attribute. Simply having the attribute # inherited by superclasses is discouraged, because if a superclass that was # only supporting one orientation, adds support for the other, its subclasses # will have to be adapted too, in general. ORIENTATION_NONE is only added for # completeness' sake. # +------------------------+--------------------+--------------------+ # | Widget bits | Horizontal bar | Vertical bar | # +========================+====================+====================+ # | ORIENTATION_NONE | ConfigError raised | ConfigError raised | # +------------------------+--------------------+--------------------+ # | ORIENTATION_HORIZONTAL | Widget displayed | ConfigError raised | # | | horizontally | | # +------------------------+--------------------+--------------------+ # | ORIENTATION_VERTICAL | ConfigError raised | Widget displayed | # | | | vertically | # +------------------------+--------------------+--------------------+ # | ORIENTATION_BOTH | Widget displayed | Widget displayed | # | | horizontally | vertically | # +------------------------+--------------------+--------------------+ class _Orientations(int): def __new__(cls, value, doc): return super(_Orientations, cls).__new__(cls, value) def __init__(self, value, doc): self.doc = doc def __str__(self): return self.doc def __repr__(self): return self.doc ORIENTATION_NONE = _Orientations(0, 'none') ORIENTATION_HORIZONTAL = _Orientations(1, 'horizontal only') ORIENTATION_VERTICAL = _Orientations(2, 'vertical only') ORIENTATION_BOTH = _Orientations(3, 'horizontal and vertical') class _Widget(command.CommandObject, configurable.Configurable): """Base Widget class If length is set to the special value `bar.STRETCH`, the bar itself will set the length to the maximum remaining space, after all other widgets have been configured. Only ONE widget per bar can have the `bar.STRETCH` length set. In horizontal bars, 'length' corresponds to the width of the widget; in vertical bars, it corresponds to the widget's height. The offsetx and offsety attributes are set by the Bar after all widgets have been configured. """ orientations = ORIENTATION_BOTH offsetx = None offsety = None defaults = [("background", None, "Widget background color")] def __init__(self, length, **config): """ length: bar.STRETCH, bar.CALCULATED, or a specified length. """ command.CommandObject.__init__(self) self.name = self.__class__.__name__.lower() if "name" in config: self.name = config["name"] configurable.Configurable.__init__(self, **config) self.add_defaults(_Widget.defaults) if length in (bar.CALCULATED, bar.STRETCH): self.length_type = length self.length = 0 else: assert isinstance(length, six.integer_types) self.length_type = bar.STATIC self.length = length self.configured = False @property def length(self): if self.length_type == bar.CALCULATED: return int(self.calculate_length()) return self._length @length.setter def length(self, value): self._length = value @property def width(self): if self.bar.horizontal: return self.length return self.bar.size @property def height(self): if self.bar.horizontal: return self.bar.size return self.length @property def offset(self): if self.bar.horizontal: return self.offsetx return self.offsety @property def win(self): return self.bar.window.window # Do not start the name with "test", or nosetests will try to test it # directly (prepend an underscore instead) def _test_orientation_compatibility(self, horizontal): if horizontal: if not self.orientations & ORIENTATION_HORIZONTAL: raise confreader.ConfigError( self.__class__.__name__ + " is not compatible with the orientation of the bar." ) elif not self.orientations & ORIENTATION_VERTICAL: raise confreader.ConfigError( self.__class__.__name__ + " is not compatible with the orientation of the bar." ) def timer_setup(self): """ This is called exactly once, after the widget has been configured and timers are available to be set up. """ pass def _configure(self, qtile, bar): self.qtile = qtile self.bar = bar self.drawer = drawer.Drawer( qtile, self.win.wid, self.bar.width, self.bar.height ) if not self.configured: self.configured = True self.qtile.call_soon(self.timer_setup) def finalize(self): if hasattr(self, 'layout') and self.layout: self.layout.finalize() self.drawer.finalize() def clear(self): self.drawer.set_source_rgb(self.bar.background) self.drawer.fillrect(self.offsetx, self.offsety, self.width, self.height) def info(self): return dict( name=self.name, offset=self.offset, length=self.length, width=self.width, height=self.height, ) def button_press(self, x, y, button): pass def button_release(self, x, y, button): pass def get(self, q, name): """ Utility function for quick retrieval of a widget by name. """ w = q.widgetMap.get(name) if not w: raise command.CommandError("No such widget: %s" % name) return w def _items(self, name): if name == "bar": return (True, None) def _select(self, name, sel): if name == "bar": return self.bar def cmd_info(self): """ Info for this object. """ return self.info() def draw(self): """ Method that draws the widget. You may call this explicitly to redraw the widget, but only if the length of the widget hasn't changed. If it has, you must call bar.draw instead. """ raise NotImplementedError def calculate_length(self): """ Must be implemented if the widget can take CALCULATED for length. It must return the width of the widget if it's installed in a horizontal bar; it must return the height of the widget if it's installed in a vertical bar. Usually you will test the orientation of the bar with 'self.bar.horizontal'. """ raise NotImplementedError def timeout_add(self, seconds, method, method_args=()): """ This method calls either ``.call_later`` with given arguments. """ return self.qtile.call_later(seconds, self._wrapper, method, *method_args) def call_process(self, command, **kwargs): """ This method uses `subprocess.check_output` to run the given command and return the string from stdout, which is decoded when using Python 3. """ output = subprocess.check_output(command, **kwargs) if six.PY3: output = output.decode() return output def _wrapper(self, method, *method_args): try: method(*method_args) except: logger.exception('got exception from widget timer') UNSPECIFIED = bar.Obj("UNSPECIFIED") class _TextBox(_Widget): """ Base class for widgets that are just boxes containing text. """ orientations = ORIENTATION_HORIZONTAL defaults = [ ("font", "Arial", "Default font"), ("fontsize", None, "Font size. Calculated if None."), ("padding", None, "Padding. Calculated if None."), ("foreground", "ffffff", "Foreground colour"), ( "fontshadow", None, "font shadow color, default is None(no shadow)" ), ("markup", False, "Whether or not to use pango markup"), ] def __init__(self, text=" ", width=bar.CALCULATED, **config): self.layout = None _Widget.__init__(self, width, **config) self.text = text self.add_defaults(_TextBox.defaults) @property def text(self): return self._text @text.setter def text(self, value): assert value is None or isinstance(value, six.string_types) self._text = value if self.layout: self.layout.text = value @property def font(self): return self._font @property def foreground(self): return self._foreground @foreground.setter def foreground(self, fg): self._foreground = fg if self.layout: self.layout.colour = fg @font.setter def font(self, value): self._font = value if self.layout: self.layout.font = value @property def fontshadow(self): return self._fontshadow @fontshadow.setter def fontshadow(self, value): self._fontshadow = value if self.layout: self.layout.font_shadow = value @property def actual_padding(self): if self.padding is None: return self.fontsize / 2 else: return self.padding def _configure(self, qtile, bar): _Widget._configure(self, qtile, bar) if self.fontsize is None: self.fontsize = self.bar.height - self.bar.height / 5 self.layout = self.drawer.textlayout( self.text, self.foreground, self.font, self.fontsize, self.fontshadow, markup=self.markup, ) def calculate_length(self): if self.text: return min( self.layout.width, self.bar.width ) + self.actual_padding * 2 else: return 0 def draw(self): # if the bar hasn't placed us yet if self.offsetx is None: return self.drawer.clear(self.background or self.bar.background) self.layout.draw( self.actual_padding or 0, int(self.bar.height / 2.0 - self.layout.height / 2.0) + 1 ) self.drawer.draw(offsetx=self.offsetx, width=self.width) def cmd_set_font(self, font=UNSPECIFIED, fontsize=UNSPECIFIED, fontshadow=UNSPECIFIED): """ Change the font used by this widget. If font is None, the current font is used. """ if font is not UNSPECIFIED: self.font = font if fontsize is not UNSPECIFIED: self.fontsize = fontsize if fontshadow is not UNSPECIFIED: self.fontshadow = fontshadow self.bar.draw() def info(self): d = _Widget.info(self) d['foreground'] = self.foreground d['text'] = self.text return d class InLoopPollText(_TextBox): """ A common interface for polling some 'fast' information, munging it, and rendering the result in a text box. You probably want to use ThreadedPollText instead. ('fast' here means that this runs /in/ the event loop, so don't block! If you want to run something nontrivial, use ThreadedPollWidget.) """ defaults = [ ("update_interval", 600, "Update interval in seconds, if none, the " "widget updates whenever the event loop is idle."), ] def __init__(self, **config): _TextBox.__init__(self, 'N/A', width=bar.CALCULATED, **config) self.add_defaults(InLoopPollText.defaults) def timer_setup(self): update_interval = self.tick() # If self.update_interval is defined and .tick() returns None, re-call # after self.update_interval if update_interval is None and self.update_interval is not None: self.timeout_add(self.update_interval, self.timer_setup) # We can change the update interval by returning something from .tick() elif update_interval: self.timeout_add(update_interval, self.timer_setup) # If update_interval is False, we won't re-call def _configure(self, qtile, bar): should_tick = self.configured _TextBox._configure(self, qtile, bar) # Update when we are being re-configured. if should_tick: self.tick() def button_press(self, x, y, button): self.tick() def poll(self): return 'N/A' def tick(self): text = self.poll() self.update(text) def update(self, text): old_width = self.layout.width if self.text != text: self.text = text # If our width hasn't changed, we just draw ourselves. Otherwise, # we draw the whole bar. if self.layout.width == old_width: self.draw() else: self.bar.draw() class ThreadedPollText(InLoopPollText): """ A common interface for polling some REST URL, munging the data, and rendering the result in a text box. """ def __init__(self, **config): InLoopPollText.__init__(self, **config) def tick(self): def worker(): try: text = self.poll() if self.qtile is not None: self.qtile.call_soon_threadsafe(self.update, text) except: logger.exception("problem polling to update widget %s", self.name) # TODO: There are nice asyncio constructs for this sort of thing, I # think... threading.Thread(target=worker).start() class ThreadPoolText(_TextBox): """ A common interface for wrapping blocking events which when triggered will update a textbox. This is an alternative to the ThreadedPollText class which differs by being push based rather than pull. The poll method is intended to wrap a blocking function which may take quite a while to return anything. It will be executed as a future and should return updated text when completed. It may also return None to disable any further updates. param: text - Initial text to display. """ defaults = [ ("update_interval", None, "Update interval in seconds, if none, the " "widget updates whenever it's done'."), ] def __init__(self, text, **config): super(ThreadPoolText, self).__init__(text, width=bar.CALCULATED, **config) self.add_defaults(ThreadPoolText.defaults) def timer_setup(self): def on_done(future): try: result = future.result() except Exception: result = None logger.exception('poll() raised exceptions, not rescheduling') if result is not None: try: self.update(result) if self.update_interval is not None: self.timeout_add(self.update_interval, self.timer_setup) else: self.timer_setup() except Exception: logger.exception('Failed to reschedule.') else: logger.warning('poll() returned None, not rescheduling') future = self.qtile.run_in_executor(self.poll) future.add_done_callback(on_done) def update(self, text): old_width = self.layout.width if self.text == text: return self.text = text if self.layout.width == old_width: self.draw() else: self.bar.draw() def poll(self): pass # these two classes below look SUSPICIOUSLY similar class PaddingMixin(object): """Mixin that provides padding(_x|_y|) To use it, subclass and add this to __init__: self.add_defaults(base.PaddingMixin.defaults) """ defaults = [ ("padding", 3, "Padding inside the box"), ("padding_x", None, "X Padding. Overrides 'padding' if set"), ("padding_y", None, "Y Padding. Overrides 'padding' if set"), ] padding_x = configurable.ExtraFallback('padding_x', 'padding') padding_y = configurable.ExtraFallback('padding_y', 'padding') class MarginMixin(object): """Mixin that provides margin(_x|_y|) To use it, subclass and add this to __init__: self.add_defaults(base.MarginMixin.defaults) """ defaults = [ ("margin", 3, "Margin inside the box"), ("margin_x", None, "X Margin. Overrides 'margin' if set"), ("margin_y", None, "Y Margin. Overrides 'margin' if set"), ] margin_x = configurable.ExtraFallback('margin_x', 'margin') margin_y = configurable.ExtraFallback('margin_y', 'margin') def deprecated(msg): warnings.warn(msg, DeprecationWarning) qtile-0.10.7/libqtile/widget/battery.py000066400000000000000000000276301305063162100200360ustar00rootroot00000000000000# Copyright (c) 2011 matt # Copyright (c) 2011 Paul Colomiets # Copyright (c) 2011-2014 Tycho Andersen # Copyright (c) 2012 dmpayton # Copyright (c) 2012 hbc # Copyright (c) 2012 Tim Neumann # Copyright (c) 2012 uberj # Copyright (c) 2012-2013 Craig Barnes # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 dequis # Copyright (c) 2014 Sebastien Blot # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import division import cairocffi import os from libqtile import bar from libqtile.log_utils import logger from . import base BAT_DIR = '/sys/class/power_supply' CHARGED = 'Full' CHARGING = 'Charging' DISCHARGING = 'Discharging' UNKNOWN = 'Unknown' BATTERY_INFO_FILES = { 'energy_now_file': ['energy_now', 'charge_now'], 'energy_full_file': ['energy_full', 'charge_full'], 'power_now_file': ['power_now', 'current_now'], 'status_file': ['status'], } def default_icon_path(): # default icons are in libqtile/resources/battery-icons root = os.sep.join(os.path.abspath(__file__).split(os.sep)[:-2]) return os.path.join(root, 'resources', 'battery-icons') class _Battery(base._TextBox): """Base battery class""" filenames = {} defaults = [ ('battery_name', 'BAT0', 'ACPI name of a battery, usually BAT0'), ( 'status_file', 'status', 'Name of status file in' ' /sys/class/power_supply/battery_name' ), ( 'energy_now_file', None, 'Name of file with the ' 'current energy in /sys/class/power_supply/battery_name' ), ( 'energy_full_file', None, 'Name of file with the maximum' ' energy in /sys/class/power_supply/battery_name' ), ( 'power_now_file', None, 'Name of file with the current' ' power draw in /sys/class/power_supply/battery_name' ), ('update_delay', 60, 'The delay in seconds between updates'), ] def __init__(self, **config): base._TextBox.__init__(self, "BAT", bar.CALCULATED, **config) self.add_defaults(_Battery.defaults) def _load_file(self, name): try: path = os.path.join(BAT_DIR, self.battery_name, name) with open(path, 'r') as f: return f.read().strip() except IOError: if name == 'current_now': return 0 return False except Exception: logger.exception("Failed to get %s" % name) def _get_param(self, name): if name in self.filenames and self.filenames[name]: return self._load_file(self.filenames[name]) elif name not in self.filenames: # Don't have the file name cached, figure it out # Don't modify the global list! Copy with [:] file_list = BATTERY_INFO_FILES.get(name, [])[:] if getattr(self, name, None): # If a file is manually specified, check it first file_list.insert(0, getattr(self, name)) # Iterate over the possibilities, and return the first valid value for file in file_list: value = self._load_file(file) if value is not False and value is not None: self.filenames[name] = file return value # If we made it this far, we don't have a valid file. # Set it to None to avoid trying the next time. self.filenames[name] = None return None def _get_info(self): try: info = { 'stat': self._get_param('status_file'), 'now': float(self._get_param('energy_now_file')), 'full': float(self._get_param('energy_full_file')), 'power': float(self._get_param('power_now_file')), } except TypeError: return False return info class Battery(_Battery): """A simple but flexible text-based battery widget""" orientations = base.ORIENTATION_HORIZONTAL defaults = [ ('charge_char', '^', 'Character to indicate the battery is charging'), ('discharge_char', 'V', 'Character to indicate the battery is discharging' ), ('error_message', 'Error', 'Error message if something is wrong'), ('format', '{char} {percent:2.0%} {hour:d}:{min:02d}', 'Display format' ), ('hide_threshold', None, 'Hide the text when there is enough energy'), ('low_percentage', 0.10, "Indicates when to use the low_foreground color 0 < x < 1" ), ('low_foreground', 'FF0000', 'Font color on low battery'), ] def __init__(self, **config): _Battery.__init__(self, **config) self.add_defaults(Battery.defaults) def timer_setup(self): update_delay = self.update() if update_delay is None and self.update_delay is not None: self.timeout_add(self.update_delay, self.timer_setup) elif update_delay: self.timeout_add(update_delay, self.timer_setup) def _configure(self, qtile, bar): if self.configured: self.update() _Battery._configure(self, qtile, bar) def _get_text(self): info = self._get_info() if info is False: return self.error_message # Set the charging character try: # hide the text when it's higher than threshold, but still # display `full` when the battery is fully charged. if self.hide_threshold and \ info['now'] / info['full'] * 100.0 >= \ self.hide_threshold and \ info['stat'] != CHARGED: return '' elif info['stat'] == DISCHARGING: char = self.discharge_char time = info['now'] / info['power'] elif info['stat'] == CHARGING: char = self.charge_char time = (info['full'] - info['now']) / info['power'] # if percent charge >0 and <50, but not discharging or charging, then return the current status # TODO: make this configurable, don't just use 50% as arbitrary cut-off, maybe check if plugged in elif info['now'] > 0 and \ info['stat'] == UNKNOWN and \ int(info['now'] / info['full']) != 1: return '~' + str(int(info['now'] / info['full'] * 100)) + '%' # battery is empty and not charging elif info['now'] == 0 and info['stat'] == UNKNOWN: return 'Empty' else: return 'Full' except ZeroDivisionError: time = -1 # Calculate the battery percentage and time left if time >= 0: hour = int(time) min = int(time * 60) % 60 else: hour = -1 min = -1 percent = info['now'] / info['full'] watt = info['power'] / 1e6 if info['stat'] == DISCHARGING and percent < self.low_percentage: self.layout.colour = self.low_foreground else: self.layout.colour = self.foreground return self.format.format( char=char, percent=percent, watt=watt, hour=hour, min=min ) def update(self): ntext = self._get_text() if ntext != self.text: self.text = ntext self.bar.draw() class BatteryIcon(_Battery): """Battery life indicator widget.""" orientations = base.ORIENTATION_HORIZONTAL defaults = [ ('theme_path', default_icon_path(), 'Path of the icons'), ('custom_icons', {}, 'dict containing key->filename icon map'), ] def __init__(self, **config): _Battery.__init__(self, **config) self.add_defaults(BatteryIcon.defaults) if self.theme_path: self.length_type = bar.STATIC self.length = 0 self.surfaces = {} self.current_icon = 'battery-missing' self.icons = dict([(x, '{0}.png'.format(x)) for x in ( 'battery-missing', 'battery-caution', 'battery-low', 'battery-good', 'battery-full', 'battery-caution-charging', 'battery-low-charging', 'battery-good-charging', 'battery-full-charging', 'battery-full-charged', )]) self.icons.update(self.custom_icons) def timer_setup(self): self.update() self.timeout_add(self.update_delay, self.timer_setup) def _configure(self, qtile, bar): base._TextBox._configure(self, qtile, bar) self.setup_images() def _get_icon_key(self): key = 'battery' info = self._get_info() if info is False or not info.get('full'): key += '-missing' else: percent = info['now'] / info['full'] if percent < .2: key += '-caution' elif percent < .4: key += '-low' elif percent < .8: key += '-good' else: key += '-full' if info['stat'] == CHARGING: key += '-charging' elif info['stat'] == CHARGED: key += '-charged' return key def update(self): icon = self._get_icon_key() if icon != self.current_icon: self.current_icon = icon self.draw() def draw(self): if self.theme_path: self.drawer.clear(self.background or self.bar.background) self.drawer.ctx.set_source(self.surfaces[self.current_icon]) self.drawer.ctx.paint() self.drawer.draw(offsetx=self.offset, width=self.length) else: self.text = self.current_icon[8:] base._TextBox.draw(self) def setup_images(self): for key, name in self.icons.items(): try: path = os.path.join(self.theme_path, name) img = cairocffi.ImageSurface.create_from_png(path) except cairocffi.Error: self.theme_path = None logger.warning('Battery Icon switching to text mode') return input_width = img.get_width() input_height = img.get_height() sp = input_height / (self.bar.height - 1) width = input_width / sp if width > self.length: # cast to `int` only after handling all potentially-float values self.length = int(width + self.actual_padding * 2) imgpat = cairocffi.SurfacePattern(img) scaler = cairocffi.Matrix() scaler.scale(sp, sp) scaler.translate(self.actual_padding * -1, 0) imgpat.set_matrix(scaler) imgpat.set_filter(cairocffi.FILTER_BEST) self.surfaces[key] = imgpat qtile-0.10.7/libqtile/widget/bitcoin_ticker.py000066400000000000000000000063461305063162100213550ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2013 Jendrik Poloczek # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 Aborilov Pavel # Copyright (c) 2014 Sean Vig # Copyright (c) 2014-2015 Tycho Andersen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from . import base from .generic_poll_text import GenPollUrl import locale class BitcoinTicker(GenPollUrl): """ A bitcoin ticker widget, data provided by the btc-e.com API. Defaults to displaying currency in whatever the current locale is. Examples: :: # display the average price of bitcoin in local currency widget.BitcoinTicker(format="BTC: {avg}") # display the average price of litecoin in local currency widget.BitcoinTicker(format="LTC: {avg}", source_currency='ltc') # display the average price of litecoin in bitcoin widget.BitcoinTicker(format="BTC: ฿{avg}", source_currency='ltc', currency='btc', round=False) """ QUERY_URL = "https://btc-e.com/api/2/%s_%s/ticker" orientations = base.ORIENTATION_HORIZONTAL defaults = [ ('currency', locale.localeconv()['int_curr_symbol'].strip(), 'The currency the value that bitcoin is displayed in'), ('source_currency', 'btc', 'The source currency to convert from'), ('round', True, 'whether or not to use locale.currency to round the values'), ('format', 'BTC Buy: {buy}, Sell: {sell}', 'Display format, allows buy, sell, high, low, avg, ' 'vol, vol_cur, last, variables.'), ] def __init__(self, **config): GenPollUrl.__init__(self, **config) self.add_defaults(BitcoinTicker.defaults) @property def url(self): return self.QUERY_URL % (self.source_currency.lower(), self.currency.lower()) def parse(self, body): formatted = {} if 'error' in body and body['error'] == "invalid pair": locale.setlocale(locale.LC_MONETARY, "en_US.UTF-8") self.currency = locale.localeconv()['int_curr_symbol'].strip() body = self.fetch(self.url) for k, v in body['ticker'].items(): formatted[k] = v if self.round: formatted[k] = locale.currency(v) return self.format.format(**formatted) qtile-0.10.7/libqtile/widget/canto.py000066400000000000000000000045701305063162100174660ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2011 Kenji_Takahashi # Copyright (c) 2011 Mounier Florian # Copyright (c) 2012, 2014 Tycho Andersen # Copyright (c) 2014-2015 Sean Vig # Copyright (c) 2014 Adi Sieker # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from . import base from subprocess import call class Canto(base.ThreadedPollText): """Display RSS feeds updates using the canto console reader""" orientations = base.ORIENTATION_HORIZONTAL defaults = [ ("fetch", False, "Whether to fetch new items on update"), ("feeds", [], "List of feeds to display, empty for all"), ("one_format", "{name}: {number}", "One feed display format"), ("all_format", "{number}", "All feeds display format"), ] def __init__(self, **config): base.ThreadedPollText.__init__(self, **config) self.add_defaults(Canto.defaults) def poll(self): if not self.feeds: arg = "-a" if self.fetch: arg += "u" output = self.all_format.format( number=self.call_process(["canto", arg])[:-1] ) return output else: if self.fetch: call(["canto", "-u"]) return "".join([self.one_format.format( name=feed, number=self.call_process(["canto", "-n", feed])[:-1] ) for feed in self.feeds]) qtile-0.10.7/libqtile/widget/check_updates.py000066400000000000000000000073201305063162100211600ustar00rootroot00000000000000# Copyright (c) 2015 Ali Mousavi # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from . import base from libqtile.log_utils import logger from subprocess import CalledProcessError, Popen class CheckUpdates(base.ThreadedPollText): """Shows number of pending updates in different unix systems""" orientations = base.ORIENTATION_HORIZONTAL defaults = [ ("distro", "Arch", "Name of your distribution"), ("update_interval", 60, "Update interval in seconds."), ('execute', None, 'Command to execute on click'), ("display_format", "Updates: {updates}", "Display format if updates available"), ("colour_no_updates", "ffffff", "Colour when there's no updates."), ("colour_have_updates", "ffffff", "Colour when there are updates.") ] def __init__(self, **config): base.ThreadedPollText.__init__(self, **config) self.add_defaults(CheckUpdates.defaults) # format: "Distro": ("cmd", "number of lines to subtract from output") self.cmd_dict = {"Arch": ("pacman -Qu", 0), "Arch_checkupdates": ("checkupdates", 0), "Arch_Sup": ("pacman -Sup", 1), "Debian": ("apt-show-versions -u -b", 0), "Ubuntu": ("aptitude search ~U", 0), "Fedora": ("dnf list updates", 3), "FreeBSD": ("pkg_version -I -l '<'", 0), "Mandriva": ("urpmq --auto-select", 0) } # Check if distro name is valid. try: self.cmd = self.cmd_dict[self.distro][0].split() self.subtr = self.cmd_dict[self.distro][1] except KeyError: distros = sorted(self.cmd_dict.keys()) logger.error(self.distro + ' is not a valid distro name. ' + 'Use one of the list: ' + str(distros) + '.') self.cmd = None def _check_updates(self): try: updates = self.call_process(self.cmd) except CalledProcessError: updates = "" num_updates = str(len(updates.splitlines()) - self.subtr) self._set_colour(num_updates) return self.display_format.format(**{"updates": num_updates}) def _set_colour(self, num_updates): if num_updates: self.layout.colour = self.colour_have_updates else: self.layout.colour = self.colour_no_updates def poll(self): if not self.cmd: return "N/A" return self._check_updates() def button_press(self, x, y, button): base.ThreadedPollText.button_press(self, x, y, button) if button == 1 and self.execute is not None: Popen(self.execute, shell=True) qtile-0.10.7/libqtile/widget/clipboard.py000066400000000000000000000102621305063162100203140ustar00rootroot00000000000000# Copyright (c) 2014 Sean Vig # Copyright (c) 2014 roger # Copyright (c) 2014 Adi Sieker # Copyright (c) 2014 Tycho Andersen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from . import base from .. import bar, hook, xcbq class Clipboard(base._TextBox): """Display current clipboard contents""" orientations = base.ORIENTATION_HORIZONTAL defaults = [ ("selection", "CLIPBOARD", "the selection to display(CLIPBOARD or PRIMARY)"), ("max_width", 10, "maximum number of characters to display " "(None for all, useful when width is bar.STRETCH)"), ("timeout", 10, "Default timeout (seconds) for display text, None to keep forever"), ("blacklist", ["keepassx"], "list with blacklisted wm_class, sadly not every " "clipboard window sets them, keepassx does." "Clipboard contents from blacklisted wm_classes " "will be replaced by the value of ``blacklist_text``."), ("blacklist_text", "***********", "text to display when the wm_class is blacklisted") ] def __init__(self, width=bar.CALCULATED, **config): base._TextBox.__init__(self, "", width, **config) self.add_defaults(Clipboard.defaults) self.timeout_id = None def _configure(self, qtile, bar): base._TextBox._configure(self, qtile, bar) self.text = "" self.setup_hooks() def clear(self, *args): self.text = "" self.bar.draw() def is_blacklisted(self, owner_id): if not self.blacklist: return False if owner_id in self.qtile.windowMap: owner = self.qtile.windowMap[owner_id].window else: owner = xcbq.Window(self.qtile.conn, owner_id) owner_class = owner.get_wm_class() if owner_class: for wm_class in self.blacklist: if wm_class in owner_class: return True def setup_hooks(self): def hook_change(name, selection): if name != self.selection: return if self.is_blacklisted(selection["owner"]): text = self.blacklist_text else: text = selection["selection"].replace("\n", " ") text = text.strip() if self.max_width is not None and len(text) > self.max_width: text = text[:self.max_width] + "..." self.text = text if self.timeout_id: self.timeout_id.cancel() self.timeout_id = None if self.timeout: self.timeout_id = self.timeout_add(self.timeout, self.clear) self.bar.draw() def hook_notify(name, selection): if name != self.selection: return if self.timeout_id: self.timeout_id.cancel() self.timeout_id = None # only clear if don't change don't apply in .5 seconds if self.timeout: self.timeout_id = self.timeout_add(self.timeout, self.clear) self.bar.draw() hook.subscribe.selection_notify(hook_notify) hook.subscribe.selection_change(hook_change) qtile-0.10.7/libqtile/widget/clock.py000066400000000000000000000054221305063162100174520ustar00rootroot00000000000000# Copyright (c) 2010 Aldo Cortesi # Copyright (c) 2012 Andrew Grigorev # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Tycho Andersen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from time import time from datetime import datetime, timedelta from contextlib import contextmanager from . import base import os @contextmanager def tz(the_tz): orig = os.environ.get('TZ') os.environ['TZ'] = the_tz yield if orig is not None: os.environ['TZ'] = orig else: del os.environ['TZ'] class Clock(base.InLoopPollText): """A simple but flexible text-based clock""" orientations = base.ORIENTATION_HORIZONTAL defaults = [ ('format', '%H:%M', 'A Python datetime format string'), ('update_interval', 1., 'Update interval for the clock'), ('timezone', None, 'The timezone to use for this clock, ' 'e.g. "US/Central" (or anything in /usr/share/zoneinfo). None means ' 'the default timezone.') ] DELTA = timedelta(seconds=0.5) def __init__(self, **config): base.InLoopPollText.__init__(self, **config) self.add_defaults(Clock.defaults) def tick(self): self.update(self.poll()) return self.update_interval - time() % self.update_interval # adding .5 to get a proper seconds value because glib could # theoreticaly call our method too early and we could get something # like (x-1).999 instead of x.000 def _get_time(self): return (datetime.now() + self.DELTA).strftime(self.format) def poll(self): # We use None as a sentinel here because C's strftime defaults to UTC # if TZ=''. if self.timezone is not None: with tz(self.timezone): return self._get_time() else: return self._get_time() qtile-0.10.7/libqtile/widget/cmus.py000066400000000000000000000112471305063162100173300ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2015, Juan Riquelme González # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import subprocess from . import base class Cmus(base.ThreadPoolText): """A simple Cmus widget. Show the artist and album of now listening song and allow basic mouse control from the bar: - toggle pause (or play if stopped) on left click; - skip forward in playlist on scroll up; - skip backward in playlist on scroll down. Cmus (https://cmus.github.io) should be installed. """ orientations = base.ORIENTATION_HORIZONTAL defaults = [ ('play_color', '00ff00', 'Text colour when playing.'), ('noplay_color', 'cecece', 'Text colour when not playing.'), ('max_chars', 0, 'Maximum number of characters to display in widget.'), ('update_interval', 0.5, 'Update Time in seconds.') ] def __init__(self, **config): base.ThreadPoolText.__init__(self, "", **config) self.add_defaults(Cmus.defaults) self.status = "" self.local = None def get_info(self): """Return a dictionary with info about the current cmus status.""" try: output = self.call_process(['cmus-remote', '-C', 'status']) except subprocess.CalledProcessError as err: output = err.output.decode() if output.startswith("status"): output = output.splitlines() info = {'status': "", 'file': "", 'artist': "", 'album': "", 'title': "", 'stream': ""} for line in output: for data in info: if data in line: index = line.index(data) if index < 5: info[data] = line[len(data) + index:].strip() break elif line.startswith("set"): return info return info def now_playing(self): """Return a string with the now playing info (Artist - Song Title).""" info = self.get_info() now_playing = "" if info: status = info['status'] if self.status != status: self.status = status if self.status == "playing": self.layout.colour = self.play_color else: self.layout.colour = self.noplay_color self.local = info['file'].startswith("/") title = info['title'] if self.local: artist = info['artist'] now_playing = "{0} - {1}".format(artist, title) else: if info['stream']: now_playing = info['stream'] else: now_playing = title if now_playing: now_playing = "♫ {0}".format(now_playing) return now_playing def update(self, text): """Update the text box.""" old_width = self.layout.width if not self.status: return if len(text) > self.max_chars > 0: text = text[:self.max_chars] + "…" self.text = text if self.layout.width == old_width: self.draw() else: self.bar.draw() def poll(self): """Poll content for the text box.""" return self.now_playing() def button_press(self, x, y, button): """What to do when press a mouse button over the cmus widget. Will: - toggle pause (or play if stopped) on left click; - skip forward in playlist on scroll up; - skip backward in playlist on scroll down. """ if button == 1: if self.status in ('playing', 'paused'): subprocess.Popen(['cmus-remote', '-u']) elif self.status == 'stopped': subprocess.Popen(['cmus-remote', '-p']) elif button == 4: subprocess.Popen(['cmus-remote', '-n']) elif button == 5: subprocess.Popen(['cmus-remote', '-r']) qtile-0.10.7/libqtile/widget/countdown.py000066400000000000000000000044551305063162100204040ustar00rootroot00000000000000# Copyright (c) 2014 Sean Vig # Copyright (c) 2014 roger # Copyright (c) 2014 Tycho Andersen # Copyright (c) 2014 Adi Sieker # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from datetime import datetime from . import base class Countdown(base.InLoopPollText): """A simple countdown timer text widget""" orientations = base.ORIENTATION_HORIZONTAL defaults = [ ('format', '{D}d {H}h {M}m {S}s', 'Format of the displayed text. Available variables:' '{D} == days, {H} == hours, {M} == minutes, {S} seconds.'), ('update_interval', 1., 'Update interval in seconds for the clock'), ('date', datetime.now(), "The datetime for the endo of the countdown"), ] def __init__(self, **config): base.InLoopPollText.__init__(self, **config) self.add_defaults(Countdown.defaults) def poll(self): now = datetime.now() days = hours = minutes = seconds = 0 if not self.date < now: delta = self.date - now days = delta.days hours, rem = divmod(delta.seconds, 3600) minutes, seconds = divmod(rem, 60) data = {"D": "%02d" % days, "H": "%02d" % hours, "M": "%02d" % minutes, "S": "%02d" % seconds} return self.format.format(**data) qtile-0.10.7/libqtile/widget/crashme.py000066400000000000000000000044431305063162100200030ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2012 Florian Mounier # Copyright (c) 2012 roger # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2013 Craig Barnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Adi Sieker # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from .. import bar from . import base class _CrashMe(base._TextBox): """A developer widget to force a crash in qtile Pressing left mouse button causes a zero divison error. Pressing the right mouse button causes a cairo draw error. Parameters ========== width : A fixed width, or bar.CALCULATED to calculate the width automatically (which is recommended). """ orientations = base.ORIENTATION_HORIZONTAL def __init__(self, width=bar.CALCULATED, **config): base._TextBox.__init__(self, "Crash me !", width, **config) def _configure(self, qtile, bar): base._Widget._configure(self, qtile, bar) self.layout = self.drawer.textlayout( self.text, self.foreground, self.font, self.fontsize, self.fontshadow, markup=True ) def button_press(self, x, y, button): if button == 1: 1 / 0 elif button == 3: self.text = '\xC3GError' self.bar.draw() qtile-0.10.7/libqtile/widget/currentlayout.py000066400000000000000000000210511305063162100212730ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2011 Florian Mounier # Copyright (c) 2011 Kenji_Takahashi # Copyright (c) 2012 roger # Copyright (c) 2012, 2014 Tycho Andersen # Copyright (c) 2012 Maximilian Köhl # Copyright (c) 2013 Craig Barnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Adi Sieker # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import division from . import base from .. import bar, hook from ..log_utils import logger import six import os import cairocffi from ..layout.base import Layout from .. import layout as layout_module class CurrentLayout(base._TextBox): """ Display the name of the current layout of the current group of the screen, the bar containing the widget, is on. """ orientations = base.ORIENTATION_HORIZONTAL def __init__(self, width=bar.CALCULATED, **config): base._TextBox.__init__(self, "", width, **config) def _configure(self, qtile, bar): base._TextBox._configure(self, qtile, bar) self.text = self.bar.screen.group.layouts[0].name self.setup_hooks() def setup_hooks(self): def hook_response(layout, group): if group.screen is not None and group.screen == self.bar.screen: self.text = layout.name self.bar.draw() hook.subscribe.layout_change(hook_response) def button_press(self, x, y, button): if button == 1: self.qtile.cmd_next_layout() elif button == 2: self.qtile.cmd_prev_layout() class CurrentLayoutIcon(base._TextBox): """ Display the icon representing the current layout of the current group of the screen on which the bar containing the widget is. If you are using custom layouts, a default icon with question mark will be displayed for them. If you want to use custom icon for your own layout, for example, `FooGrid`, then create a file named "layout-foogrid.png" and place it in `~/.icons` directory. You can as well use other directories, but then you need to specify those directories in `custom_icon_paths` argument for this plugin. The order of icon search is: - dirs in `custom_icon_paths` config argument - `~/.icons` - built-in qtile icons """ orientations = base.ORIENTATION_HORIZONTAL defaults = [ ( 'scale', 1, 'Scale factor relative to the bar height. ' 'Defaults to 1' ), ( 'custom_icon_paths', [], 'List of folders where to search icons before' 'using built-in icons or icons in ~/.icons dir. ' 'This can also be used to provide' 'missing icons for custom layouts. ' 'Defaults to empty list.' ) ] def __init__(self, **config): base._TextBox.__init__(self, "", **config) self.add_defaults(CurrentLayoutIcon.defaults) self.scale = 1.0 / self.scale self.length_type = bar.STATIC self.length = 0 def _configure(self, qtile, bar): base._TextBox._configure(self, qtile, bar) self.text = self.bar.screen.group.layouts[0].name self.current_layout = self.text self.icons_loaded = False self.icon_paths = [] self.surfaces = {} self._update_icon_paths() self._setup_images() self._setup_hooks() def _setup_hooks(self): """ Listens for layout change and performs a redraw when it occurs. """ def hook_response(layout, group): if group.screen is not None and group.screen == self.bar.screen: self.current_layout = layout.name self.bar.draw() hook.subscribe.layout_change(hook_response) def button_press(self, x, y, button): if button == 1: self.qtile.cmd_next_layout() elif button == 2: self.qtile.cmd_prev_layout() def draw(self): if self.icons_loaded: try: surface = self.surfaces[self.current_layout] except KeyError: logger.error('No icon for layout {}'.format( self.current_layout )) else: self.drawer.clear(self.background or self.bar.background) self.drawer.ctx.set_source(surface) self.drawer.ctx.paint() self.drawer.draw(offsetx=self.offset, width=self.length) else: # Fallback to text self.text = self.current_layout[0].upper() base._TextBox.draw(self) def _get_layout_names(self): """ Returns the list of lowercased strings for each available layout name. """ return [ layout_class_name.lower() for layout_class, layout_class_name in map(lambda x: (getattr(layout_module, x), x), dir(layout_module)) if isinstance(layout_class, six.class_types) and issubclass(layout_class, Layout) ] def _update_icon_paths(self): self.icon_paths = [] # We allow user to override icon search path self.icon_paths.extend(self.custom_icon_paths) # We also look in ~/.icons/ self.icon_paths.append(os.path.expanduser('~/.icons')) # Default icons are in libqtile/resources/layout-icons. # If using default config without any custom icons, # this path will be used. root = os.sep.join(os.path.abspath(__file__).split(os.sep)[:-2]) self.icon_paths.append(os.path.join(root, 'resources', 'layout-icons')) def find_icon_file_path(self, layout_name): icon_filename = 'layout-{}.png'.format(layout_name) for icon_path in self.icon_paths: icon_file_path = os.path.join(icon_path, icon_filename) if os.path.isfile(icon_file_path): return icon_file_path def _setup_images(self): """ Loads layout icons. """ for layout_name in self._get_layout_names(): icon_file_path = self.find_icon_file_path(layout_name) if icon_file_path is None: logger.warning('No icon found for layout "{}"'.format(layout_name)) icon_file_path = self.find_icon_file_path('unknown') try: img = cairocffi.ImageSurface.create_from_png(icon_file_path) except (cairocffi.Error, IOError) as e: # Icon file is guaranteed to exist at this point. # If this exception happens, it means the icon file contains # an invalid image or is not readable. self.icons_loaded = False logger.exception( 'Failed to load icon from file "{}", ' 'error was: {}'.format(icon_file_path, e.message) ) return input_width = img.get_width() input_height = img.get_height() sp = input_height / (self.bar.height - 1) width = input_width / sp if width > self.length: self.length = int(width) + self.actual_padding * 2 imgpat = cairocffi.SurfacePattern(img) scaler = cairocffi.Matrix() scaler.scale(sp, sp) scaler.scale(self.scale, self.scale) factor = (1 - 1 / self.scale) / 2 scaler.translate(-width * factor, -width * factor) scaler.translate(self.actual_padding * -1, 0) imgpat.set_matrix(scaler) imgpat.set_filter(cairocffi.FILTER_BEST) self.surfaces[layout_name] = imgpat self.icons_loaded = True qtile-0.10.7/libqtile/widget/currentscreen.py000066400000000000000000000045561305063162100212500ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2015 Alexander Fasching # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from . import base from .. import bar, hook class CurrentScreen(base._TextBox): """Indicates whether the screen this widget is on is currently active or not""" defaults = [ ('active_text', 'A', 'Text displayed when the screen is active'), ('inactive_text', 'I', 'Text displayed when the screen is inactive'), ('active_color', '00ff00', 'Color when screen is active'), ('inactive_color', 'ff0000', 'Color when screen is inactive') ] orientations = base.ORIENTATION_HORIZONTAL def __init__(self, width=bar.CALCULATED, **config): base._TextBox.__init__(self, "", width, **config) self.add_defaults(CurrentScreen.defaults) def _configure(self, qtile, bar): base._TextBox._configure(self, qtile, bar) self.update_text() self.setup_hooks() def setup_hooks(self): def hook_response(): self.update_text() self.bar.draw() hook.subscribe.current_screen_change(hook_response) def update_text(self): if self.qtile.currentScreen == self.bar.screen: self.text = self.active_text self.foreground = self.active_color else: self.text = self.inactive_text self.foreground = self.inactive_color qtile-0.10.7/libqtile/widget/debuginfo.py000066400000000000000000000056551305063162100203310ustar00rootroot00000000000000# Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Thomas Sarboni # Copyright (c) 2014-2015 Tycho Andersen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from .. import hook, bar, layout from . import base class DebugInfo(base._TextBox): """Displays debugging infos about selected window""" orientations = base.ORIENTATION_HORIZONTAL def __init__(self, **config): base._TextBox.__init__(self, text=" ", width=bar.CALCULATED, **config) def _configure(self, qtile, bar): base._TextBox._configure(self, qtile, bar) hook.subscribe.focus_change(self.update) hook.subscribe.layout_change(self.update) hook.subscribe.float_change(self.update) def update(self, *args): old_layout_width = self.layout.width w = self.bar.screen.group.currentWindow if isinstance(w.group.layout, layout.Stack): stack = w.group.layout.currentStack stackOffset = w.group.layout.currentStackOffset idx = stack.lst.index(w) current = stack.current self.text = "Stack: %s Idx: %s Cur: %s" % (stackOffset, idx, current) elif isinstance(w.group.layout, layout.TreeTab): node = w.group.layout._nodes[w] nodeIdx = node.parent.children.index(node) snode = node level = 1 while not isinstance(snode, layout.tree.Section): snode = snode.parent level += 1 sectionIdx = snode.parent.children.index(snode) self.text = "Level: %s SectionIdx: %s NodeIdx: %s" % (level, sectionIdx, nodeIdx) if self.layout.width != old_layout_width: self.bar.draw() else: self.draw() qtile-0.10.7/libqtile/widget/df.py000066400000000000000000000056121305063162100167510ustar00rootroot00000000000000# -*- coding:utf-8 -*- # Copyright (c) 2015, Roger Duran. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import os from . import base class DF(base.ThreadedPollText): """Disk Free Widget By default the widget only displays if the space is less than warn_space. """ orientations = base.ORIENTATION_HORIZONTAL defaults = [ ('partition', '/', 'the partition to check space'), ('warn_color', 'ff0000', 'Warning color'), ('warn_space', 2, 'Warning space in scale defined by the ``measure`` option.'), ('visible_on_warn', True, 'Only display if warning'), ('measure', "G", "Measurement (G, M, B)"), ('format', '{p} ({uf}{m})', 'String format (p: partition, s: size, ' 'f: free space, uf: user free space, m: measure)'), ('update_interval', 60, 'The update interval.'), ] measures = {"G": 1024 * 1024 * 1024, "M": 1024 * 1024, "B": 1024} def __init__(self, **config): base.ThreadedPollText.__init__(self, **config) self.add_defaults(DF.defaults) self.user_free = 0 self.calc = self.measures[self.measure] def draw(self): if self.user_free <= self.warn_space: self.layout.colour = self.warn_color else: self.layout.colour = self.foreground base.ThreadedPollText.draw(self) def poll(self): statvfs = os.statvfs(self.partition) size = statvfs.f_frsize * statvfs.f_blocks // self.calc free = statvfs.f_frsize * statvfs.f_bfree // self.calc self.user_free = statvfs.f_frsize * statvfs.f_bavail // self.calc if self.visible_on_warn and self.user_free >= self.warn_space: text = "" else: text = self.format.format(p=self.partition, s=size, f=free, uf=self.user_free, m=self.measure) return text qtile-0.10.7/libqtile/widget/generic_poll_text.py000066400000000000000000000052721305063162100220700ustar00rootroot00000000000000import json import six from six.moves.urllib.request import urlopen, Request from libqtile.widget import base from libqtile.log_utils import logger try: import xmltodict def xmlparse(body): return xmltodict.parse(body) except ImportError: # TODO: we could implement a similar parser by hand, but i'm lazy, so let's # punt for now def xmlparse(body): raise Exception("no xmltodict library") class GenPollText(base.ThreadedPollText): """A generic text widget that polls using poll function to get the text""" orientations = base.ORIENTATION_HORIZONTAL defaults = [ ('func', None, 'Poll Function'), ] def __init__(self, **config): base.ThreadedPollText.__init__(self, **config) self.add_defaults(GenPollText.defaults) def poll(self): if not self.func: return "You need a poll function" return self.func() class GenPollUrl(base.ThreadedPollText): """A generic text widget that polls an url and parses it using parse function""" orientations = base.ORIENTATION_HORIZONTAL defaults = [ ('url', None, 'Url'), ('data', None, 'Post Data'), ('parse', None, 'Parse Function'), ('json', True, 'Is Json?'), ('user_agent', 'Qtile', 'Set the user agent'), ('headers', {}, 'Extra Headers'), ('xml', False, 'Is XML?'), ] def __init__(self, **config): base.ThreadedPollText.__init__(self, **config) self.add_defaults(GenPollUrl.defaults) def fetch(self, url, data=None, headers=None, is_json=True, is_xml=False): if headers is None: headers = {} req = Request(url, data, headers) res = urlopen(req) if six.PY3: charset = res.headers.get_content_charset() else: charset = res.headers.getparam('charset') body = res.read() if charset: body = body.decode(charset) if is_json: body = json.loads(body) if is_xml: body = xmlparse(body) return body def poll(self): if not self.parse or not self.url: return "Invalid config" data = self.data headers = {"User-agent": self.user_agent} if self.json: headers['Content-Type'] = 'application/json' if data and not isinstance(data, str): data = json.dumps(data).encode() headers.update(self.headers) body = self.fetch(self.url, data, headers, self.json, self.xml) try: text = self.parse(body) except Exception: logger.exception('got exception polling widget') text = "Can't parse" return text qtile-0.10.7/libqtile/widget/gmail_checker.py000066400000000000000000000051521305063162100211340ustar00rootroot00000000000000# Copyright (c) 2014 Sean Vig # Copyright (c) 2014 zordsdavini # Copyright (c) 2014 Alexandr Kriptonov # Copyright (c) 2014 Tycho Andersen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from libqtile.log_utils import logger from . import base import imaplib import re class GmailChecker(base.ThreadedPollText): """A simple gmail checker""" orientations = base.ORIENTATION_HORIZONTAL defaults = [ ("update_interval", 30, "Update time in seconds."), ("username", None, "username"), ("password", None, "password"), ("email_path", "INBOX", "email_path"), ("fmt", "inbox[%s],unseen[%s]", "fmt"), ("status_only_unseen", False, "Only show unseen messages"), ] def __init__(self, **config): base._TextBox.__init__(self, **config) self.add_defaults(GmailChecker.defaults) def poll(self): self.gmail = imaplib.IMAP4_SSL('imap.gmail.com') self.gmail.login(self.username, self.password) answer, raw_data = self.gmail.status(self.email_path, '(MESSAGES UNSEEN)') if answer == "OK": dec = raw_data[0].decode() messages = int(re.search('MESSAGES\s+(\d+)', dec).group(1)) unseen = int(re.search('UNSEEN\s+(\d+)', dec).group(1)) if(self.status_only_unseen): return self.fmt % unseen else: return self.fmt % (messages, unseen) else: logger.exception( 'GmailChecker UNKNOWN error, answer: %s, raw_data: %s', answer, raw_data) return "UNKNOWN ERROR" qtile-0.10.7/libqtile/widget/graph.py000066400000000000000000000341661305063162100174670ustar00rootroot00000000000000# Copyright (c) 2010 Aldo Cortesi # Copyright (c) 2010-2011 Paul Colomiets # Copyright (c) 2010, 2014 roger # Copyright (c) 2011 Mounier Florian # Copyright (c) 2011 Kenji_Takahashi # Copyright (c) 2012 Mika Fischer # Copyright (c) 2012, 2014-2015 Tycho Andersen # Copyright (c) 2012-2013 Craig Barnes # Copyright (c) 2013 dequis # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2013 Mickael FALCK # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Adi Sieker # Copyright (c) 2014 Florian Scherf # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import cairocffi from . import base from libqtile.log_utils import logger from os import statvfs import time import platform __all__ = [ 'CPUGraph', 'MemoryGraph', 'SwapGraph', 'NetGraph', 'HDDGraph', 'HDDBusyGraph', ] class _Graph(base._Widget): fixed_upper_bound = False defaults = [ ("graph_color", "18BAEB", "Graph color"), ("fill_color", "1667EB.3", "Fill color for linefill graph"), ("border_color", "215578", "Widget border color"), ("border_width", 2, "Widget border width"), ("margin_x", 3, "Margin X"), ("margin_y", 3, "Margin Y"), ("samples", 100, "Count of graph samples."), ("frequency", 1, "Update frequency in seconds"), ("type", "linefill", "'box', 'line', 'linefill'"), ("line_width", 3, "Line width"), ("start_pos", "bottom", "Drawer starting position ('bottom'/'top')"), ] def __init__(self, width=100, **config): base._Widget.__init__(self, width, **config) self.add_defaults(_Graph.defaults) self.values = [0] * self.samples self.maxvalue = 0 self.oldtime = time.time() self.lag_cycles = 0 def timer_setup(self): self.timeout_add(self.frequency, self.update) @property def graphwidth(self): return self.width - self.border_width * 2 - self.margin_x * 2 @property def graphheight(self): return self.bar.height - self.margin_y * 2 - self.border_width * 2 def draw_box(self, x, y, values): step = self.graphwidth / float(self.samples) self.drawer.set_source_rgb(self.graph_color) for val in values: val = self.val(val) self.drawer.fillrect(x, y - val, step, val) x += step def draw_line(self, x, y, values): step = self.graphwidth / float(self.samples - 1) self.drawer.ctx.set_line_join(cairocffi.LINE_JOIN_ROUND) self.drawer.set_source_rgb(self.graph_color) self.drawer.ctx.set_line_width(self.line_width) for val in values: self.drawer.ctx.line_to(x, y - self.val(val)) x += step self.drawer.ctx.stroke() def draw_linefill(self, x, y, values): step = self.graphwidth / float(self.samples - 2) self.drawer.ctx.set_line_join(cairocffi.LINE_JOIN_ROUND) self.drawer.set_source_rgb(self.graph_color) self.drawer.ctx.set_line_width(self.line_width) for index, val in enumerate(values): self.drawer.ctx.line_to(x + index * step, y - self.val(val)) self.drawer.ctx.stroke_preserve() self.drawer.ctx.line_to( x + (len(values) - 1) * step, y - 1 + self.line_width / 2.0 ) self.drawer.ctx.line_to(x, y - 1 + self.line_width / 2.0) self.drawer.set_source_rgb(self.fill_color) self.drawer.ctx.fill() def val(self, val): if self.start_pos == 'bottom': return val elif self.start_pos == 'top': return -val else: raise ValueError("Unknown starting position: %s." % self.start_pos) def draw(self): self.drawer.clear(self.background or self.bar.background) if self.border_width: self.drawer.set_source_rgb(self.border_color) self.drawer.ctx.set_line_width(self.border_width) self.drawer.ctx.rectangle( self.margin_x + self.border_width / 2.0, self.margin_y + self.border_width / 2.0, self.graphwidth + self.border_width, self.bar.height - self.margin_y * 2 - self.border_width, ) self.drawer.ctx.stroke() x = self.margin_x + self.border_width y = self.margin_y + self.border_width if self.start_pos == 'bottom': y += self.graphheight elif not self.start_pos == 'top': raise ValueError("Unknown starting position: %s." % self.start_pos) k = 1.0 / (self.maxvalue or 1) scaled = [self.graphheight * val * k for val in reversed(self.values)] if self.type == "box": self.draw_box(x, y, scaled) elif self.type == "line": self.draw_line(x, y, scaled) elif self.type == "linefill": self.draw_linefill(x, y, scaled) else: raise ValueError("Unknown graph type: %s." % self.type) self.drawer.draw(offsetx=self.offset, width=self.width) def push(self, value): if self.lag_cycles > self.samples: # compensate lag by sending the same value up to # the graph samples limit self.lag_cycles = 1 self.values = ([value] * min(self.samples, self.lag_cycles)) + self.values self.values = self.values[:self.samples] if not self.fixed_upper_bound: self.maxvalue = max(self.values) self.draw() def update(self): # lag detection newtime = time.time() self.lag_cycles = int((newtime - self.oldtime) / self.frequency) self.oldtime = newtime self.update_graph() self.timeout_add(self.frequency, self.update) def fulfill(self, value): self.values = [value] * len(self.values) class CPUGraph(_Graph): """Display CPU usage graph""" orientations = base.ORIENTATION_HORIZONTAL defaults = [ ("core", "all", "Which core to show (all/0/1/2/...)"), ] fixed_upper_bound = True def __init__(self, **config): _Graph.__init__(self, **config) self.add_defaults(CPUGraph.defaults) self.maxvalue = 100 self.oldvalues = self._getvalues() def _getvalues(self): proc = '/proc/stat' if platform.system() == "FreeBSD": proc = '/compat/linux' + proc with open(proc) as file: lines = file.readlines() # default to all cores (first line) line = lines.pop(0) # core specified, grab the corresponding line if isinstance(self.core, int): # we already removed the first line from the list, # so it's 0 indexed now :D line = lines[self.core] if not line.startswith("cpu%s" % self.core): raise ValueError("No such core: %s" % self.core) if platform.system() == 'FreeBSD': name, user, nice, sys, idle = line.split(None, 4) else: name, user, nice, sys, idle, iowait, tail = line.split(None, 6) return (int(user), int(nice), int(sys), int(idle)) def update_graph(self): nval = self._getvalues() oval = self.oldvalues busy = nval[0] + nval[1] + nval[2] - oval[0] - oval[1] - oval[2] total = busy + nval[3] - oval[3] # sometimes this value is zero for unknown reason (time shift?) # we just sent the previous value, because it gives us no info about # cpu load, if it's zero. if total: push_value = busy * 100.0 / total self.push(push_value) else: self.push(self.values[0]) self.oldvalues = nval def get_meminfo(): val = {} proc = '/proc/meminfo' if platform.system() == "FreeBSD": proc = "/compat/linux" + proc with open(proc) as file: for line in file: if line.lstrip().startswith("total"): pass else: key, tail = line.strip().split(':') uv = tail.split() val[key] = int(uv[0]) val['MemUsed'] = val['MemTotal'] - val['MemFree'] return val class MemoryGraph(_Graph): """Displays a memory usage graph""" orientations = base.ORIENTATION_HORIZONTAL fixed_upper_bound = True def __init__(self, **config): _Graph.__init__(self, **config) val = self._getvalues() self.maxvalue = val['MemTotal'] mem = val['MemTotal'] - val['MemFree'] - val['Buffers'] - val['Cached'] self.fulfill(mem) def _getvalues(self): return get_meminfo() def update_graph(self): val = self._getvalues() self.push( val['MemTotal'] - val['MemFree'] - val['Buffers'] - val['Cached'] ) class SwapGraph(_Graph): """Display a swap info graph""" orientations = base.ORIENTATION_HORIZONTAL fixed_upper_bound = True def __init__(self, **config): _Graph.__init__(self, **config) val = self._getvalues() self.maxvalue = val['SwapTotal'] swap = val['SwapTotal'] - val['SwapFree'] - val.get('SwapCached', 0) self.fulfill(swap) def _getvalues(self): return get_meminfo() def update_graph(self): val = self._getvalues() swap = val['SwapTotal'] - val['SwapFree'] - val.get('SwapCached', 0) # can change, swapon/off if self.maxvalue != val['SwapTotal']: self.maxvalue = val['SwapTotal'] self.fulfill(swap) self.push(swap) class NetGraph(_Graph): """Display a network usage graph""" orientations = base.ORIENTATION_HORIZONTAL defaults = [ ( "interface", "auto", "Interface to display info for ('auto' for detection)" ), ("bandwidth_type", "down", "down(load)/up(load)"), ] def __init__(self, **config): _Graph.__init__(self, **config) self.add_defaults(NetGraph.defaults) if self.interface == "auto": try: self.interface = self.get_main_iface() except RuntimeError: logger.warning( "NetGraph - Automatic interface detection failed, " "falling back to 'eth0'" ) self.interface = "eth0" self.filename = '/sys/class/net/{interface}/statistics/{type}'.format( interface=self.interface, type=self.bandwidth_type == 'down' and 'rx_bytes' or 'tx_bytes' ) self.bytes = 0 self.bytes = self._getValues() def _getValues(self): try: with open(self.filename) as file: val = int(file.read()) rval = val - self.bytes self.bytes = val return rval except IOError: return 0 def update_graph(self): val = self._getValues() self.push(val) @staticmethod def get_main_iface(): def make_route(line): return dict(zip(['iface', 'dest'], line.split())) with open('/proc/net/route', 'r') as fp: lines = fp.readlines() routes = [make_route(line) for line in lines[1:]] try: return next( (r for r in routes if not int(r['dest'], 16)), routes[0] )['iface'] except (KeyError, IndexError, ValueError): raise RuntimeError('No valid interfaces available') class HDDGraph(_Graph): """Display HDD free or used space graph""" fixed_upper_bound = True orientations = base.ORIENTATION_HORIZONTAL defaults = [ ("path", "/", "Partition mount point."), ("space_type", "used", "free/used") ] def __init__(self, **config): _Graph.__init__(self, **config) self.add_defaults(HDDGraph.defaults) stats = statvfs(self.path) self.maxvalue = stats.f_blocks * stats.f_frsize values = self._getValues() self.fulfill(values) def _getValues(self): stats = statvfs(self.path) if self.space_type == 'used': return (stats.f_blocks - stats.f_bfree) * stats.f_frsize else: return stats.f_bavail * stats.f_frsize def update_graph(self): val = self._getValues() self.push(val) class HDDBusyGraph(_Graph): """Display HDD busy time graph Parses /sys/block//stat file and extracts overall device IO usage, based on ``io_ticks``'s value. See https://www.kernel.org/doc/Documentation/block/stat.txt """ orientations = base.ORIENTATION_HORIZONTAL defaults = [ ("device", "sda", "Block device to display info for") ] def __init__(self, **config): _Graph.__init__(self, **config) self.add_defaults(HDDBusyGraph.defaults) self.path = '/sys/block/{dev}/stat'.format( dev=self.device ) self._prev = 0 def _getActivity(self): try: # io_ticks is field number 9 with open(self.path) as f: io_ticks = int(f.read().split()[9]) except IOError: return 0 activity = io_ticks - self._prev self._prev = io_ticks return activity def update_graph(self): self.push(self._getActivity()) qtile-0.10.7/libqtile/widget/groupbox.py000066400000000000000000000300131305063162100202160ustar00rootroot00000000000000# Copyright (c) 2008, 2010 Aldo Cortesi # Copyright (c) 2009 Ben Duffield # Copyright (c) 2010 aldo # Copyright (c) 2010-2012 roger # Copyright (c) 2011 Florian Mounier # Copyright (c) 2011 Kenji_Takahashi # Copyright (c) 2011-2015 Tycho Andersen # Copyright (c) 2012-2013 dequis # Copyright (c) 2012 Craig Barnes # Copyright (c) 2013 xarvh # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Filipe Nepomuceno # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import itertools from .. import bar, hook from . import base class _GroupBase(base._TextBox, base.PaddingMixin, base.MarginMixin): defaults = [ ("borderwidth", 3, "Current group border width"), ("center_aligned", False, "center-aligned group box"), ] def __init__(self, **config): base._TextBox.__init__(self, width=bar.CALCULATED, **config) self.add_defaults(_GroupBase.defaults) self.add_defaults(base.PaddingMixin.defaults) self.add_defaults(base.MarginMixin.defaults) def box_width(self, groups): width, _ = self.drawer.max_layout_size( [i.name for i in groups], self.font, self.fontsize ) return width + self.padding_x * 2 + self.borderwidth * 2 def _configure(self, qtile, bar): base._Widget._configure(self, qtile, bar) if self.fontsize is None: calc = self.bar.height - self.margin_y * 2 - \ self.borderwidth * 2 - self.padding_y * 2 self.fontsize = max(calc, 1) self.layout = self.drawer.textlayout( "", "ffffff", self.font, self.fontsize, self.fontshadow ) self.setup_hooks() def setup_hooks(self): def hook_response(*args, **kwargs): self.bar.draw() hook.subscribe.client_managed(hook_response) hook.subscribe.client_urgent_hint_changed(hook_response) hook.subscribe.client_killed(hook_response) hook.subscribe.setgroup(hook_response) hook.subscribe.group_window_add(hook_response) hook.subscribe.current_screen_change(hook_response) hook.subscribe.changegroup(hook_response) def drawbox(self, offset, text, bordercolor, textcolor, highlight_color=None, width=None, rounded=False, block=False, line=False, highlighted=False): self.layout.text = text self.layout.font_family = self.font self.layout.font_size = self.fontsize self.layout.colour = textcolor if width is not None: self.layout.width = width if line: pad_y = [ (self.bar.height - self.layout.height - self.borderwidth) / 2, (self.bar.height - self.layout.height + self.borderwidth) / 2 ] else: pad_y = self.padding_y framed = self.layout.framed( self.borderwidth, bordercolor, 0, pad_y, highlight_color ) y = self.margin_y if self.center_aligned: for t in base.MarginMixin.defaults: if t[0] == 'margin': y += (self.bar.height - framed.height) / 2 - t[1] break if block: framed.draw_fill(offset, y, rounded) elif line: framed.draw_line(offset, y, highlighted) else: framed.draw(offset, y, rounded) class AGroupBox(_GroupBase): """A widget that graphically displays the current group""" orientations = base.ORIENTATION_HORIZONTAL defaults = [("border", "000000", "group box border color")] def __init__(self, **config): _GroupBase.__init__(self, **config) self.add_defaults(AGroupBox.defaults) def button_press(self, x, y, button): self.bar.screen.cmd_next_group() def calculate_length(self): return self.box_width(self.qtile.groups) + self.margin_x * 2 def draw(self): self.drawer.clear(self.background or self.bar.background) e = next( i for i in self.qtile.groups if i.name == self.bar.screen.group.name ) self.drawbox(self.margin_x, e.name, self.border, self.foreground) self.drawer.draw(offsetx=self.offset, width=self.width) class GroupBox(_GroupBase): """A widget that graphically displays the current group""" orientations = base.ORIENTATION_HORIZONTAL defaults = [ ("active", "FFFFFF", "Active group font colour"), ("inactive", "404040", "Inactive group font colour"), ( "highlight_method", "border", "Method of highlighting ('border', 'block', 'text', or 'line')" "Uses \*_border color settings" ), ("rounded", True, "To round or not to round box borders"), ( "this_current_screen_border", "215578", "Border or line colour for group on this screen when focused." ), ( "this_screen_border", "215578", "Border or line colour for group on this screen when unfocused." ), ( "other_current_screen_border", "404040", "Border or line colour for group on other screen when focused." ), ( "other_screen_border", "404040", "Border or line colour for group on other screen when unfocused." ), ( "highlight_color", ["000000", "282828"], "Active group highlight color when using 'line' highlight method." ), ( "urgent_alert_method", "border", "Method for alerting you of WM urgent " "hints (one of 'border', 'text', 'block', or 'line')" ), ("urgent_text", "FF0000", "Urgent group font color"), ("urgent_border", "FF0000", "Urgent border or line color"), ( "disable_drag", False, "Disable dragging and dropping of group names on widget" ), ("invert_mouse_wheel", False, "Whether to invert mouse wheel group movement"), ("use_mouse_wheel", True, "Whether to use mouse wheel events"), ( "visible_groups", None, "Groups that will be visible " "(if set to None or [], all groups will be visible)" ), ( "spacing", None, "Spacing between groups" "(if set to None, will be equal to margin_x)") ] def __init__(self, **config): _GroupBase.__init__(self, **config) self.add_defaults(GroupBox.defaults) if self.spacing is None: self.spacing = self.margin_x self.clicked = None @property def groups(self): return self.qtile.groups if not self.visible_groups else \ [g for g in self.qtile.groups if g.name in self.visible_groups] def get_clicked_group(self, x, y): group = None new_width = self.margin_x - self.spacing / 2.0 width = 0 for g in self.groups: new_width += self.box_width([g]) + self.spacing if width <= x <= new_width: group = g break width = new_width return group def button_press(self, x, y, button): self.clicked = None group = None curGroup = self.qtile.currentGroup if button == (5 if not self.invert_mouse_wheel else 4): if self.use_mouse_wheel: i = itertools.cycle(self.qtile.groups) while next(i) != curGroup: pass while group is None or group not in self.groups: group = next(i) elif button == (4 if not self.invert_mouse_wheel else 5): if self.use_mouse_wheel: i = itertools.cycle(reversed(self.qtile.groups)) while next(i) != curGroup: pass while group is None or group not in self.groups: group = next(i) else: group = self.get_clicked_group(x, y) if not self.disable_drag: self.clicked = group if group: self.bar.screen.setGroup(group) def button_release(self, x, y, button): if button not in (5, 4): group = self.get_clicked_group(x, y) if group and self.clicked: group.cmd_switch_groups(self.clicked.name) self.clicked = None def calculate_length(self): width = self.margin_x * 2 + (len(self.groups) - 1) * self.spacing for g in self.groups: width += self.box_width([g]) return width def group_has_urgent(self, group): return len([w for w in group.windows if w.urgent]) > 0 def draw(self): self.drawer.clear(self.background or self.bar.background) offset = self.margin_x for i, g in enumerate(self.groups): to_highlight = False is_block = (self.highlight_method == 'block') is_line = (self.highlight_method == 'line') bw = self.box_width([g]) if self.group_has_urgent(g) and self.urgent_alert_method == "text": text_color = self.urgent_text elif g.windows: text_color = self.active else: text_color = self.inactive if g.screen: if self.highlight_method == 'text': border = self.bar.background text_color = self.this_current_screen_border else: if self.bar.screen.group.name == g.name: if self.qtile.currentScreen == self.bar.screen: border = self.this_current_screen_border to_highlight = True else: border = self.this_screen_border else: if self.qtile.currentScreen == g.screen: border = self.other_current_screen_border else: border = self.other_screen_border elif self.group_has_urgent(g) and \ self.urgent_alert_method in ('border', 'block', 'line'): border = self.urgent_border if self.urgent_alert_method == 'block': is_block = True elif self.urgent_alert_method == 'line': is_line = True else: border = self.background or self.bar.background self.drawbox( offset, g.name, border, text_color, highlight_color=self.highlight_color, width=bw, rounded=self.rounded, block=is_block, line=is_line, highlighted=to_highlight ) offset += bw + self.spacing self.drawer.draw(offsetx=self.offset, width=self.width) qtile-0.10.7/libqtile/widget/idlerpg.py000066400000000000000000000042261305063162100200060ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2016 Tycho Andersen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from . import base from .generic_poll_text import GenPollUrl import datetime class IdleRPG(GenPollUrl): """ A widget for monitoring and displaying IdleRPG stats. :: # display idlerpg stats for the player 'pants' on freenode's #idlerpg widget.IdleRPG(url="http://xethron.lolhosting.net/xml.php?player=pants") """ orientations = base.ORIENTATION_HORIZONTAL defaults = [ ('format', 'IdleRPG: {online} TTL: {ttl}', 'Display format'), ('json', False, 'Not json :)'), ('xml', True, 'Is XML :)'), ] def __init__(self, **config): GenPollUrl.__init__(self, **config) self.add_defaults(IdleRPG.defaults) def parse(self, body): formatted = {} for k, v in body['player'].items(): if k == 'ttl': formatted[k] = str(datetime.timedelta(seconds=int(v))) elif k == 'online': formatted[k] = "online" if v == "1" else "offline" else: formatted[k] = v return self.format.format(**formatted) qtile-0.10.7/libqtile/widget/image.py000066400000000000000000000104211305063162100174340ustar00rootroot00000000000000# Copyright (c) 2013 dequis # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Adi Sieker # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import division import os import cairocffi from . import base from .. import bar class Image(base._Widget, base.MarginMixin): """Display a PNG image on the bar""" orientations = base.ORIENTATION_BOTH defaults = [ ("scale", True, "Enable/Disable image scaling"), ("filename", None, "PNG Image filename. Can contain '~'"), ] def __init__(self, length=bar.CALCULATED, width=None, **config): # 'width' was replaced by 'length' since the widget can be installed in # vertical bars if width is not None: base.deprecated('width kwarg or positional argument is ' 'deprecated. Please use length.') length = width base._Widget.__init__(self, length, **config) self.add_defaults(Image.defaults) self.add_defaults(base.MarginMixin.defaults) # make the default 0 instead self._widget_defaults["margin"] = 0 def _configure(self, qtile, bar): base._Widget._configure(self, qtile, bar) if not self.filename: raise ValueError("Filename not set!") self.filename = os.path.expanduser(self.filename) try: self.image = cairocffi.ImageSurface.create_from_png(self.filename) except MemoryError: raise ValueError("The image '%s' doesn't seem to be a valid PNG" % (self.filename)) self.pattern = cairocffi.SurfacePattern(self.image) self.image_width = self.image.get_width() self.image_height = self.image.get_height() if self.scale: if self.bar.horizontal: new_height = self.bar.height - (self.margin_y * 2) if new_height and self.image_height != new_height: scaler = cairocffi.Matrix() sp = self.image_height / new_height self.image_height = new_height self.image_width = int(self.image_width / sp) scaler.scale(sp, sp) self.pattern.set_matrix(scaler) else: new_width = self.bar.width - (self.margin_x * 2) if new_width and self.image_width != new_width: scaler = cairocffi.Matrix() sp = self.image_width / new_width self.image_width = new_width self.image_height = int(self.image_height / sp) scaler.scale(sp, sp) self.pattern.set_matrix(scaler) def draw(self): self.drawer.clear(self.bar.background) self.drawer.ctx.save() self.drawer.ctx.translate(self.margin_x, self.margin_y) self.drawer.ctx.set_source(self.pattern) self.drawer.ctx.paint() self.drawer.ctx.restore() if self.bar.horizontal: self.drawer.draw(offsetx=self.offset, width=self.width) else: self.drawer.draw(offsety=self.offset, height=self.width) def calculate_length(self): if self.bar.horizontal: return self.image_width + (self.margin_x * 2) else: return self.image_height + (self.margin_y * 2) qtile-0.10.7/libqtile/widget/imapwidget.py000066400000000000000000000067041305063162100205150ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2015 David R. Andersen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from . import base from libqtile.log_utils import logger import imaplib import re import keyring class ImapWidget(base.ThreadedPollText): """Email IMAP widget This widget will scan one of your imap email boxes and report the number of unseen messages present. I've configured it to only work with imap with ssl. Your password is obtained from the Gnome Keyring. Writing your password to the keyring initially is as simple as (changing out and for your userid and password): 1) create the file ~/.local/share/python_keyring/keyringrc.cfg with the following contents: [backend] default-keyring=keyring.backends.Gnome.Keyring keyring-path=/home//.local/share/keyring/ 2) Execute the following python shell script once: #!/usr/bin/env python3 import keyring user = password = keyring.set_password('imapwidget', user, password) mbox names must include the path to the mbox (except for the default INBOX). So, for example if your mailroot is ~/Maildir, and you want to look at the mailbox at HomeMail/fred, the mbox setting would be: mbox='"~/Maildir/HomeMail/fred"'. Note the nested sets of quotes! Labels can be whatever you choose, of course. """ orientations = base.ORIENTATION_HORIZONTAL defaults = [ ('mbox', '"INBOX"', 'mailbox to fetch'), ('label', 'INBOX', 'label for display'), ('user', None, 'email username'), ('server', None, 'email server name'), ] def __init__(self, **config): base.ThreadedPollText.__init__(self, **config) self.add_defaults(ImapWidget.defaults) password = keyring.get_password('imapwidget', self.user) if password is not None: self.password = password else: logger.critical('Gnome Keyring Error') def poll(self): im = imaplib.IMAP4_SSL(self.server, 993) if self.password == 'Gnome Keyring Error': self.text = 'Gnome Keyring Error' else: im.login(self.user, self.password) status, response = im.status(self.mbox, '(UNSEEN)') self.text = response[0].decode() self.text = self.label + ': ' + re.sub(r'\).*$', '', re.sub(r'^.*N\s', '', self.text)) im.logout() return self.text qtile-0.10.7/libqtile/widget/keyboardkbdd.py000066400000000000000000000071051305063162100210040ustar00rootroot00000000000000# Copyright (c) 2015 Ali Mousavi # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from . import base from libqtile.log_utils import logger from dbus.mainloop.glib import DBusGMainLoop import re import dbus class KeyboardKbdd(base.ThreadedPollText): """Widget for changing keyboard layouts per window, using kbdd kbdd should be installed and running, you can get it from: https://github.com/qnikst/kbdd """ orientations = base.ORIENTATION_HORIZONTAL defaults = [ ("update_interval", 1, "Update interval in seconds."), ("configured_keyboards", ["us", "ir"], "your predefined list of keyboard layouts." "example: ['us', 'ir', 'es']"), ("colours", None, "foreground colour for each layout" "either 'None' or a list of colours." "example: ['ffffff', 'E6F0AF']. ") ] def __init__(self, **config): base.ThreadedPollText.__init__(self, **config) self.add_defaults(KeyboardKbdd.defaults) self.keyboard = self.configured_keyboards[0] self.is_kbdd_running = self._check_kbdd() if not self.is_kbdd_running: logger.error('Please check if kbdd is running') self.keyboard = "N/A" self._dbus_init() def _check_kbdd(self): running_list = self.call_process(["ps", "axw"]) if re.search("kbdd", running_list): self.keyboard = self.configured_keyboards[0] return True return False def _dbus_init(self): dbus_loop = DBusGMainLoop() bus = dbus.SessionBus(mainloop=dbus_loop) bus.add_signal_receiver(self._layout_changed, dbus_interface='ru.gentoo.kbdd', signal_name='layoutChanged') def _layout_changed(self, layout_changed): """ Handler for "layoutChanged" dbus signal. """ if self.colours: self._set_colour(layout_changed) self.keyboard = self.configured_keyboards[layout_changed] def _set_colour(self, index): if isinstance(self.colours, list): try: self.layout.colour = self.colours[index] except ValueError: self._setColour(index - 1) else: logger.error('variable "colours" should be a list, to set a\ colour for all layouts, use "foreground".') def poll(self): if not self.is_kbdd_running: if self._check_kbdd(): self.is_kbdd_running = True return self.configured_keyboards[0] return self.keyboard qtile-0.10.7/libqtile/widget/keyboardlayout.py000066400000000000000000000106541305063162100214200ustar00rootroot00000000000000# Copyright (c) 2013 Jacob Mourelos # Copyright (c) 2014 Shepilov Vladislav # Copyright (c) 2014-2015 Sean Vig # Copyright (c) 2014 Tycho Andersen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import re from subprocess import CalledProcessError from . import base from libqtile.log_utils import logger kb_layout_regex = re.compile('layout:\s+(?P\w+)') kb_variant_regex = re.compile('variant:\s+(?P\w+)') class KeyboardLayout(base.InLoopPollText): """Widget for changing and displaying the current keyboard layout It requires setxkbmap to be available in the system. """ orientations = base.ORIENTATION_HORIZONTAL defaults = [ ("update_interval", 1, "Update time in seconds."), ("configured_keyboards", ["us"], "A list of predefined keyboard layouts " "represented as strings. For example: " "['us', 'us colemak', 'es', 'fr']."), ] def __init__(self, **config): base.InLoopPollText.__init__(self, **config) self.add_defaults(KeyboardLayout.defaults) def button_press(self, x, y, button): if button == 1: self.next_keyboard() def next_keyboard(self): """Set the next layout in the list of configured keyboard layouts as new current layout in use If the current keyboard layout is not in the list, it will set as new layout the first one in the list. """ current_keyboard = self.keyboard if current_keyboard in self.configured_keyboards: # iterate the list circularly next_keyboard = self.configured_keyboards[ (self.configured_keyboards.index(current_keyboard) + 1) % len(self.configured_keyboards)] else: next_keyboard = self.configured_keyboards[0] self.keyboard = next_keyboard self.tick() def poll(self): return self.keyboard.upper() def get_keyboard_layout(self, setxkbmap_output): match_layout = kb_layout_regex.search(setxkbmap_output) match_variant = kb_variant_regex.search(setxkbmap_output) if match_layout is None: return 'ERR' kb = match_layout.group('layout') if match_variant: kb += " " + match_variant.group('variant') return kb @property def keyboard(self): """Return the currently used keyboard layout as a string Examples: "us", "us dvorak". In case of error returns "unknown". """ try: command = 'setxkbmap -verbose 10' setxkbmap_output = self.call_process(command.split(' ')) keyboard = self.get_keyboard_layout(setxkbmap_output) return str(keyboard) except CalledProcessError as e: logger.error('Can not get the keyboard layout: {0}'.format(e)) except OSError as e: logger.error('Please, check that xset is available: {0}'.format(e)) return "unknown" @keyboard.setter def keyboard(self, keyboard): command = ['setxkbmap'] command.extend(keyboard.split(" ")) try: self.call_process(command) except CalledProcessError as e: logger.error('Can not change the keyboard layout: {0}'.format(e)) except OSError as e: logger.error('Please, check that setxkbmap is available: {0}'.format(e)) def cmd_next_keyboard(self): """Select next keyboard layout""" self.next_keyboard() qtile-0.10.7/libqtile/widget/khal_calendar.py000066400000000000000000000124531305063162100211310ustar00rootroot00000000000000# -*- coding: utf-8 -*- ################################################################### # This widget will display the next appointment on your calendar in # the qtile status bar. Appointments within the "reminder" time will be # highlighted. Authentication credentials are stored on disk. # # This widget uses the khal command line calendar utility available at # https://github.com/geier/khal # # This widget also requires the dateutil.parser module. # If you get a strange "AttributeError: 'module' object has no attribute # GoogleCalendar" error, you are probably missing a module. Check # carefully. # # Thanks to the creator of the YahooWeather widget (dmpayton). This code # borrows liberally from that one. # # Copyright (c) 2016 by David R. Andersen # New khal output format adjustment, 2016 Christoph Lassner # Licensed under the Gnu Public License # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. ################################################################### from . import base import datetime import dateutil.parser import subprocess import string from libqtile import utils class KhalCalendar(base.ThreadedPollText): """Khal calendar widget This widget will display the next appointment on your Khal calendar in the qtile status bar. Appointments within the "reminder" time will be highlighted. """ orientations = base.ORIENTATION_HORIZONTAL defaults = [ ( 'reminder_color', 'FF0000', 'color of calendar entries during reminder time' ), ('foreground', 'FFFF33', 'default foreground color'), ('remindertime', 10, 'reminder time in minutes'), ('lookahead', 7, 'days to look ahead in the calendar'), ] def __init__(self, **config): base.ThreadedPollText.__init__(self, **config) self.add_defaults(KhalCalendar.defaults) self.text = 'Calendar not initialized.' self.default_foreground = self.foreground def poll(self): # get today and tomorrow now = datetime.datetime.now() tomorrow = now + datetime.timedelta(days=1) # get reminder time in datetime format remtime = datetime.timedelta(minutes=self.remindertime) # parse khal output for the next seven days # and get the next event args = ['khal', 'agenda', '--days', str(self.lookahead)] cal = subprocess.Popen(args, stdout=subprocess.PIPE) output = cal.communicate()[0].decode('utf-8') output = output.split('\n') if len(output) < 2: return 'No appointments scheduled' date = 'unknown' endtime = None for i in range(len(output)): # pylint: disable=consider-using-enumerate if output[i].strip() == '': continue try: starttime = dateutil.parser.parse(date + ' ' + output[i][:5], ignoretz=True) endtime = dateutil.parser.parse(date + ' ' + output[i][6:11], ignoretz=True) except ValueError: try: if output[i] == 'Today:': date = str(now.month) + '/' + str(now.day) + '/' + \ str(now.year) elif output[i] == 'Tomorrow:': date = str(tomorrow.month) + '/' + str(tomorrow.day) + \ '/' + str(tomorrow.year) else: dateutil.parser.parse(output[i]) date = output[i] continue except ValueError: pass # no date. if endtime is not None and endtime > now: data = date.replace(':', '') + ' ' + output[i] break else: data = 'No appointments in next ' + \ str(self.lookahead) + ' days' # get rid of any garbage in appointment added by khal data = ''.join(filter(lambda x: x in string.printable, data)) # colorize the event if it is within reminder time if (starttime - remtime <= now) and (endtime > now): self.foreground = utils.hex(self.reminder_color) else: self.foreground = self.default_foreground return data qtile-0.10.7/libqtile/widget/launchbar.py000066400000000000000000000220441305063162100203150ustar00rootroot00000000000000# Copyright (c) 2014 Tycho Andersen # Copyright (c) 2014 dequis # Copyright (c) 2014-2015 Joseph Razik # Copyright (c) 2014 Sean Vig # Copyright (c) 2015 reus # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """ This module define a widget that displays icons to launch softwares or commands when clicked -- a launchbar. Only png icon files are displayed, not xpm because cairo doesn't support loading of xpm file. The order of displaying (from left to right) is in the order of the list. If no icon was found for the name provided and if default_icon is set to None then the name is printed instead. If default_icon is defined then this icon is displayed instead. To execute a software: - ('thunderbird', 'thunderbird -safe-mode', 'launch thunderbird in safe mode') To execute a python command in qtile, begin with by 'qshell:' - ('logout', 'qshell:self.qtile.cmd_shutdown()', 'logout from qtile') """ from __future__ import division from libqtile import bar from libqtile.log_utils import logger from libqtile.widget import base import os.path import cairocffi from xdg.IconTheme import getIconPath class LaunchBar(base._Widget): """A widget that display icons to launch the associated command Parameters ========== progs : a list of tuples ``(software_name, command_to_execute, comment)``, for example:: ('thunderbird', 'thunderbird -safe-mode', 'launch thunderbird in safe mode') ('logout', 'qshell:self.qtile.cmd_shutdown()', 'logout from qtile') """ orientations = base.ORIENTATION_HORIZONTAL defaults = [ ('padding', 2, 'Padding between icons'), ('default_icon', '/usr/share/icons/oxygen/256x256/mimetypes/' 'application-x-executable.png', 'Default icon not found'), ] def __init__(self, progs=None, width=bar.CALCULATED, **config): base._Widget.__init__(self, width, **config) if progs is None: progs = [] self.add_defaults(LaunchBar.defaults) self.surfaces = {} self.icons_files = {} self.icons_widths = {} self.icons_offsets = {} # For now, ignore the comments but may be one day it will be useful self.progs = dict(enumerate([{'name': prog[0], 'cmd': prog[1], 'comment': prog[2] if len(prog) > 2 else None} for prog in progs])) self.progs_name = set([prog['name'] for prog in self.progs.values()]) self.length_type = bar.STATIC self.length = 0 def _configure(self, qtile, pbar): base._Widget._configure(self, qtile, pbar) self.lookup_icons() self.setup_images() self.length = self.calculate_length() def setup_images(self): """ Create image structures for each icon files. """ for img_name, iconfile in self.icons_files.items(): if iconfile is None: logger.warning( 'No icon found for application "%s" (%s) switch to text mode', img_name, iconfile) # if no icon is found and no default icon was set, we just # print the name, based on a textbox. textbox = base._TextBox() textbox._configure(self.qtile, self.bar) textbox.layout = self.drawer.textlayout( textbox.text, textbox.foreground, textbox.font, textbox.fontsize, textbox.fontshadow, markup=textbox.markup, ) # the name will be displayed textbox.text = img_name textbox.calculate_length() self.icons_widths[img_name] = textbox.width self.surfaces[img_name] = textbox continue else: try: img = cairocffi.ImageSurface.create_from_png(iconfile) except cairocffi.Error: logger.exception('Error loading icon for application "%s" (%s)', img_name, iconfile) return input_width = img.get_width() input_height = img.get_height() sp = input_height / (self.bar.height - 4) width = int(input_width / sp) imgpat = cairocffi.SurfacePattern(img) scaler = cairocffi.Matrix() scaler.scale(sp, sp) scaler.translate(self.padding * -1, -2) imgpat.set_matrix(scaler) imgpat.set_filter(cairocffi.FILTER_BEST) self.surfaces[img_name] = imgpat self.icons_widths[img_name] = width def _lookup_icon(self, name): """ Search for the icon corresponding to one command. """ self.icons_files[name] = None # if the software_name is directly an absolute path icon file if os.path.isabs(name): # name start with '/' thus it's an absolute path root, ext = os.path.splitext(name) if ext == '.png': self.icons_files[name] = name if os.path.isfile(name) else None else: # try to add the extension self.icons_files[name] = name + '.png' if os.path.isfile(name + '.png') else None else: self.icons_files[name] = getIconPath(name) # no search method found an icon, so default icon if self.icons_files[name] is None: self.icons_files[name] = self.default_icon def lookup_icons(self): """ Search for the icons corresponding to the commands to execute. """ if self.default_icon is not None: if not os.path.isfile(self.default_icon): # if the default icon provided is not found, switch to # text mode self.default_icon = None for name in self.progs_name: self._lookup_icon(name) def get_icon_in_position(self, x, y): """ Determine which icon is clicked according to its position. """ for i in self.progs: if x < (self.icons_offsets[i] + self.icons_widths[self.progs[i]['name']] + self.padding / 2): return i def button_press(self, x, y, button): """ Launch the associated command to the clicked icon. """ if button == 1: icon = self.get_icon_in_position(x, y) if icon is not None: cmd = self.progs[icon]['cmd'] if cmd.startswith('qshell:'): exec(cmd[4:].lstrip()) else: self.qtile.cmd_spawn(cmd) self.draw() def draw(self): """ Draw the icons in the widget. """ self.drawer.clear(self.background or self.bar.background) xoffset = 0 for i in sorted(self.progs.keys()): self.icons_offsets[i] = xoffset + self.padding name = self.progs[i]['name'] icon_width = self.icons_widths[name] self.drawer.ctx.move_to(self.offset + xoffset, icon_width) self.drawer.clear(self.background or self.bar.background) if isinstance(self.surfaces[name], base._TextBox): # display the name if no icon was found and no default icon textbox = self.surfaces[name] textbox.layout.draw( self.padding + textbox.actual_padding, int((self.bar.height - textbox.layout.height) / 2.0) + 1 ) else: # display an icon self.drawer.ctx.set_source(self.surfaces[name]) self.drawer.ctx.paint() self.drawer.draw(offsetx=self.offset + xoffset, width=icon_width + self.padding) xoffset += icon_width + self.padding def calculate_length(self): """ Compute the width of the widget according to each icon width. """ return sum(self.icons_widths[prg['name']] for prg in self.progs.values()) \ + self.padding * (len(self.progs) + 1) qtile-0.10.7/libqtile/widget/maildir.py000066400000000000000000000066011305063162100200000ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2011 Timo Schmiade # Copyright (c) 2012 Phil Jackson # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Tycho Andersen # Copyright (c) 2016 Christoph Lassner # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from . import base import six import os.path import mailbox class Maildir(base.ThreadedPollText): """A simple widget showing the number of new mails in maildir mailboxes""" orientations = base.ORIENTATION_HORIZONTAL defaults = [ ("maildirPath", "~/Mail", "path to the Maildir folder"), ("subFolders", [], 'The subfolders to scan (e.g. [{"path": "INBOX", ' '"label": "Home mail"}, {"path": "spam", "label": "Home junk"}]'), ("separator", " ", "the string to put between the subfolder strings."), ] def __init__(self, **config): base.ThreadedPollText.__init__(self, **config) self.add_defaults(Maildir.defaults) # if it looks like a list of strings then we just convert them # and use the name as the label if isinstance(self.subFolders[0], six.string_types): self.subFolders = [ {"path": folder, "label": folder} for folder in self.subFolders ] def poll(self): """Scans the mailbox for new messages Returns ======= A string representing the current mailbox state """ state = {} def to_maildir_fmt(paths): for path in iter(paths): yield path.rsplit(":")[0] for subFolder in self.subFolders: path = os.path.join(os.path.expanduser(self.maildirPath), subFolder["path"]) maildir = mailbox.Maildir(path) state[subFolder["label"]] = 0 for file in to_maildir_fmt(os.listdir(os.path.join(path, "new"))): if file in maildir: state[subFolder["label"]] += 1 return self.format_text(state) def format_text(self, state): """Converts the state of the subfolders to a string Parameters ========== state: a dictionary as returned by mailbox_state Returns ======= a string representation of the given state """ return self.separator.join( "{0}: {1}".format(*item) for item in state.items() ) qtile-0.10.7/libqtile/widget/memory.py000066400000000000000000000035331305063162100176700ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2015 Jörg Thalheim (Mic92) # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import division from libqtile.widget import base def get_meminfo(): val = {} with open('/proc/meminfo') as file: for line in file: key, tail = line.split(':') uv = tail.split() val[key] = int(uv[0]) // 1000 val['MemUsed'] = val['MemTotal'] - val['MemFree'] return val class Memory(base.InLoopPollText): """Displays memory usage""" orientations = base.ORIENTATION_HORIZONTAL defaults = [ ("fmt", "{MemUsed}M/{MemTotal}M", "see /proc/meminfo for field names") ] def __init__(self, **config): super(Memory, self).__init__(**config) self.add_defaults(Memory.defaults) def poll(self): return self.fmt.format(**get_meminfo()) qtile-0.10.7/libqtile/widget/moc.py000066400000000000000000000106621305063162100171370ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2015, zordsdavini # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from . import base import os import subprocess class Moc(base.ThreadPoolText): """A simple MOC widget. Show the artist and album of now listening song and allow basic mouse control from the bar: - toggle pause (or play if stopped) on left click; - skip forward in playlist on scroll up; - skip backward in playlist on scroll down. MOC (http://moc.daper.net) should be installed. """ orientations = base.ORIENTATION_HORIZONTAL defaults = [ ('play_color', '00ff00', 'Text colour when playing.'), ('noplay_color', 'cecece', 'Text colour when not playing.'), ('max_chars', 0, 'Maximum number of characters to display in widget.'), ('update_interval', 0.5, 'Update Time in seconds.'), ] def __init__(self, **config): base.ThreadPoolText.__init__(self, "", **config) self.add_defaults(Moc.defaults) self.status = "" self.local = None def get_info(self): """Return a dictionary with info about the current MOC status.""" try: output = self.call_process(['mocp', '-i']) except subprocess.CalledProcessError as err: output = err.output.decode() if output.startswith("State"): output = output.splitlines() info = {'State': "", 'File': "", 'SongTitle': "", 'Artist': "", 'Album': ""} for line in output: for data in info: if data in line: info[data] = line[len(data) + 2:].strip() break return info def now_playing(self): """Return a string with the now playing info (Artist - Song Title).""" info = self.get_info() now_playing = "" if info: status = info['State'] if self.status != status: self.status = status if self.status == "PLAY": self.layout.colour = self.play_color else: self.layout.colour = self.noplay_color title = info['SongTitle'] artist = info['Artist'] if title and artist: now_playing = "♫ {0} - {1}".format(artist, title) elif title: now_playing = "♫ {0}".format(title) else: basename = os.path.basename(info['File']) filename = os.path.splitext(basename)[0] now_playing = "♫ {0}".format(filename) if self.status == "STOP": now_playing = "♫" return now_playing def update(self, text): """Update the text box.""" old_width = self.layout.width if not self.status: return if len(text) > self.max_chars > 0: text = text[:self.max_chars] + "…" self.text = text if self.layout.width == old_width: self.draw() else: self.bar.draw() def poll(self): """Poll content for the text box.""" return self.now_playing() def button_press(self, x, y, button): """What to do when press a mouse button over the MOC widget. Will: - toggle pause (or play if stopped) on left click; - skip forward in playlist on scroll up; - skip backward in playlist on scroll down. """ if button == 1: if self.status in ('PLAY', 'PAUSE'): subprocess.Popen(['mocp', '-G']) elif self.status == 'STOP': subprocess.Popen(['mocp', '-p']) elif button == 4: subprocess.Popen(['mocp', '-f']) elif button == 5: subprocess.Popen(['mocp', '-r']) qtile-0.10.7/libqtile/widget/mpd2widget.py000066400000000000000000000131011305063162100204160ustar00rootroot00000000000000from . import base from six import u, text_type from socket import error as socket_error from mpd import MPDClient, ConnectionError, CommandError # Shortcuts # TODO: Volume inc/dec support keys = { # Left mouse button "toggle": 1, # Right mouse button "stop": 3, # Scroll up "previous": 4, # Scroll down "next": 5, # User defined command "command": None } # To display mpd state play_states = { 'play': u('\u25b6'), 'pause': u('\u23F8'), 'stop': u('\u25a0'), } def option(char): def _convert(elements, key, space): if key in elements and elements[key] != '0': elements[key] = char else: elements[key] = space return _convert prepare_status = { 'repeat': option('r'), 'random': option('z'), 'single': option('1'), 'consume': option('c'), 'updating_db': option('U') } default_format = '{play_status} {artist}/{title} ' +\ '[{repeat}{random}{single}{consume}{updating_db}]' class Mpd2(base.ThreadPoolText): """A widget for Music Player Daemon (MPD) based on python-mpd2 This widget exists since python-mpd library is no more supported. Parameters ========== status_format : format string to display status Full list of values see in ``status`` and ``currentsong`` commands https://musicpd.org/doc/protocol/command_reference.html#command_status https://musicpd.org/doc/protocol/tags.html Default:: {play_status} {artist}/{title} [{repeat}{random}{single}{consume}{updating_db}] ``play_status`` is string from ``play_states`` dict Note that ``time`` property of song renamed to ``fulltime`` to prevent conflicts with status information during formating. prepare_status : dict of functions for replace values in status with custom ``f(status, key, space_element) => str`` """ orientations = base.ORIENTATION_HORIZONTAL defaults = [ ('update_interval', 1, 'Interval of update widget'), ('host', 'localhost', 'Host of mpd server'), ('port', 6600, 'Port of mpd server'), ('password', None, 'Password for auth on mpd server'), ('keys', keys, 'Shortcut keys'), ('play_states', play_states, 'Play state mapping'), ('command', None, 'Executable command by "command" shortcut'), ('timeout', 30, 'MPDClient timeout'), ('idletimeout', 5, 'MPDClient idle command timeout'), ('no_connection', 'No connection', 'Text when mpd is disconnected'), ('space', '-', 'Space keeper') ] def __init__(self, status_format=default_format, prepare_status=prepare_status, **config): super(Mpd2, self).__init__(None, **config) self.add_defaults(Mpd2.defaults) self.status_format = status_format self.prepare_status = prepare_status self.connected = False self.client = MPDClient() self.client.timeout = self.timeout self.client.idletimeout = self.idletimeout self.try_reconnect() def try_reconnect(self): if not self.connected: try: self.client.ping() except(socket_error, ConnectionError): try: self.client.connect(self.host, self.port) if self.password: self.client.password(self.password) self.connected = True except(socket_error, ConnectionError, CommandError): self.connected = False def poll(self): self.try_reconnect() if self.connected: return self.update_status() else: return self.no_connection def update_status(self): self.client.command_list_ok_begin() self.client.status() self.client.currentsong() status, current_song = self.client.command_list_end() return self.formatter(status, current_song) # TODO: Resolve timeouts on the method call def button_press(self, x, y, button): self.try_reconnect() if self.connected: self[button] def __getitem__(self, key): if key == self.keys["toggle"]: status = self.client.status() play_status = status['state'] if play_status == 'play': self.client.pause() else: self.client.play() if key == self.keys["stop"]: self.client.stop() if key == self.keys["previous"]: self.client.previous() if key == self.keys["next"]: self.client.next() if key == self.keys['command']: if self.command: self.command(self.client) self.update(self.update_status) def formatter(self, status, currentsong): play_status = self.play_states[status['state']] # Dirty hack to prevent keys conflict currentsong['fulltime'] = currentsong['time'] del currentsong['time'] self.prepare_formatting(status, currentsong) status.update(currentsong) fmt = self.status_format if not isinstance(fmt, text_type): fmt = u(fmt) return fmt.format(play_status=play_status, **status) def prepare_formatting(self, status, currentsong): for key in self.prepare_status: self.prepare_status[key](status, key, self.space) def finalize(self): super(Mpd2, self).finalize() try: self.client.close() self.client.disconnect() except ConnectionError: pass qtile-0.10.7/libqtile/widget/mpdwidget.py000066400000000000000000000251651305063162100203510ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2010 matt # Copyright (c) 2010 Dieter Plaetinck # Copyright (c) 2010, 2012 roger # Copyright (c) 2011-2012 Florian Mounier # Copyright (c) 2011 Mounier Florian # Copyright (c) 2011 Timo Schmiade # Copyright (c) 2012 Mikkel Oscar Lyderik # Copyright (c) 2012, 2014 Tycho Andersen # Copyright (c) 2012 Craig Barnes # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2013 Tom Hunt # Copyright (c) 2014 Justin Bronder # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # depends on python-mpd # TODO: check if UI hangs in case of network issues and such # TODO: some kind of templating to make shown info configurable # TODO: best practice to handle failures? just write to stderr? from __future__ import division import re import time import mpd from .. import utils, pangocffi from . import base from libqtile.log_utils import logger class Mpd(base.ThreadPoolText): """A widget for the Music Player Daemon (MPD)""" orientations = base.ORIENTATION_HORIZONTAL defaults = [ ("host", "localhost", "Host to connect to, can be either an IP " "address or a UNIX socket path"), ("port", 6600, "Port to connect to"), ("password", None, "Password to use"), ("fmt_playing", "%a - %t [%v%%]", "Format string to display when " "playing/paused"), ("fmt_stopped", "Stopped [%v%%]", "Format strings to display when " "stopped"), ("msg_nc", "Mpd off", "Which message to show when we're not " "connected"), ("do_color_progress", True, "Whether to indicate progress in song by " "altering message color"), ("foreground_progress", "ffffff", "Foreground progress colour"), ("reconnect", False, "Attempt to reconnect if initial connection failed"), ("reconnect_interval", 1, "Time to delay between connection attempts."), ("update_interval", 0.5, "Update Time in seconds.") ] def __init__(self, **config): super(Mpd, self).__init__('MPD Widget', **config) self.add_defaults(Mpd.defaults) self.inc = 2 self.client = mpd.MPDClient() self.connected = None self.stop = False def finalize(self): self.stop = True if self.connected: try: # The volume settings is kind of a dirty trick. There doesn't # seem to be a decent way to set a timeout for the idle # command. Therefore we need to trigger some events such that # if poll() is currently waiting on an idle event it will get # something so that it can exit. In practice, I can't tell the # difference in volume and hopefully no one else can either. self.client.volume(1) self.client.volume(-1) self.client.disconnect() except: pass base._Widget.finalize(self) def connect(self, quiet=False): if self.connected: return True try: self.client.connect(host=self.host, port=self.port) except Exception: if not quiet: logger.exception('Failed to connect to mpd') return False if self.password: try: self.client.password(self.password) except Exception: logger.warning('Authentication failed. Disconnecting') try: self.client.disconnect() except Exception: pass return False self.connected = True return True def _configure(self, qtile, bar): super(Mpd, self)._configure(qtile, bar) self.layout = self.drawer.textlayout( self.text, self.foreground, self.font, self.fontsize, self.fontshadow, markup=True ) def to_minutes_seconds(self, stime): """Takes an integer time in seconds, transforms it into (HH:)?MM:SS. HH portion is only visible if total time is greater than an hour. """ if type(stime) != int: stime = int(stime) mm = stime // 60 ss = stime % 60 if mm >= 60: hh = mm // 60 mm = mm % 60 rv = "{}:{:02}:{:02}".format(hh, mm, ss) else: rv = "{}:{:02}".format(mm, ss) return rv def get_artist(self): return self.song['artist'] def get_album(self): return self.song['album'] def get_elapsed(self): elapsed = self.status['time'].split(':')[0] return self.to_minutes_seconds(elapsed) def get_file(self): return self.song['file'] def get_length(self): return self.to_minutes_seconds(self.song['time']) def get_number(self): return str(int(self.status['song']) + 1) def get_playlistlength(self): return self.status['playlistlength'] def get_status(self): n = self.status['state'] if n == "play": return "->" elif n == "pause": return "||" elif n == "stop": return "[]" def get_longstatus(self): n = self.status['state'] if n == "play": return "Playing" elif n == "pause": return "Paused" elif n == "stop": return "Stopped" def get_title(self): return self.song['title'] def get_track(self): # This occasionally has leading zeros we don't want. return str(int(self.song['track'].split('/')[0])) def get_volume(self): return self.status['volume'] def get_single(self): if self.status['single'] == '1': return '1' else: return '_' def get_repeat(self): if self.status['repeat'] == '1': return 'R' else: return '_' def get_shuffle(self): if self.status['random'] == '1': return 'S' else: return '_' formats = { 'a': get_artist, 'A': get_album, 'e': get_elapsed, 'f': get_file, 'l': get_length, 'n': get_number, 'p': get_playlistlength, 's': get_status, 'S': get_longstatus, 't': get_title, 'T': get_track, 'v': get_volume, '1': get_single, 'r': get_repeat, 'h': get_shuffle, '%': lambda x: '%', } def match_check(self, m): try: return self.formats[m.group(1)](self) except KeyError: return "(nil)" def do_format(self, string): return re.sub("%(.)", self.match_check, string) def _get_status(self): playing = self.msg_nc try: self.status = self.client.status() self.song = self.client.currentsong() if self.status['state'] != 'stop': text = self.do_format(self.fmt_playing) if self.do_color_progress and self.status.get('time'): try: elapsed, total = self.status['time'].split(':') percent = float(elapsed) / float(total) progress = int(percent * len(text)) except (ZeroDivisionError, ValueError): playing = pangocffi.markup_escape_text(text) else: playing = '{1}{2}'.format( utils.hex(self.foreground_progress), pangocffi.markup_escape_text(text[:progress]), pangocffi.markup_escape_text(text[progress:]) ) else: playing = pangocffi.markup_escape_text(text) else: playing = self.do_format(self.fmt_stopped) except Exception: logger.exception('Mpd error on update') return playing def poll(self): was_connected = self.connected if not self.connected: if self.connected is None or self.reconnect: while not self.stop and not self.connect(quiet=True): time.sleep(self.reconnect_interval) else: return if self.stop: return True if was_connected: try: self.client.ping() except mpd.ConnectionError: self.client.disconnect() self.connected = False return self.msg_nc except Exception: logger.exception('Error communicating with mpd') self.client.disconnect() return return self._get_status() def button_press(self, x, y, button): if not self.connect(): return False try: status = self.client.status() if button == 3: if not status or status.get('state', '') == 'stop': self.client.play() else: self.client.pause() elif button == 4: self.client.previous() elif button == 5: self.client.next() elif button == 8: if status: self.client.setvol( max(int(status['volume']) - self.inc, 0) ) elif button == 9: if status: self.client.setvol( min(int(status['volume']) + self.inc, 100) ) except Exception: logger.exception('Mpd error on click') qtile-0.10.7/libqtile/widget/mpris2widget.py000066400000000000000000000151511305063162100207770ustar00rootroot00000000000000# Copyright (c) 2014 Sebastian Kricner # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Adi Sieker # Copyright (c) 2014 Tycho Andersen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import dbus from dbus.mainloop.glib import DBusGMainLoop from . import base class Mpris2(base._TextBox): """An MPRIS 2 widget A widget which displays the current track/artist of your favorite MPRIS player. It should work with all MPRIS 2 compatible players which implement a reasonably correct version of MPRIS, though I have only tested it with audacious. This widget scrolls the text if neccessary and information that is displayed is configurable. """ orientations = base.ORIENTATION_HORIZONTAL defaults = [ ('name', 'audacious', 'Name of the MPRIS widget.'), ('objname', 'org.mpris.MediaPlayer2.audacious', 'DBUS MPRIS 2 compatible player identifier' '- Find it out with dbus-monitor - ' 'Also see: http://specifications.freedesktop.org/' 'mpris-spec/latest/#Bus-Name-Policy'), ('display_metadata', ['xesam:title', 'xesam:album', 'xesam:artist'], 'Which metadata identifiers to display. ' 'See http://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata/#index5h3 ' 'for available values'), ('scroll_chars', 30, 'How many chars at once to display.'), ('scroll_interval', 0.5, 'Scroll delay interval.'), ('scroll_wait_intervals', 8, 'Wait x scroll_interval before' 'scrolling/removing text'), ('stop_pause_text', None, "Optional text to display when in the stopped/paused state"), ] def __init__(self, **config): base._TextBox.__init__(self, '', **config) self.add_defaults(self.__class__.defaults) dbus_loop = DBusGMainLoop() bus = dbus.SessionBus(mainloop=dbus_loop) bus.add_signal_receiver(self.update, 'PropertiesChanged', 'org.freedesktop.DBus.Properties', self.objname, '/org/mpris/MediaPlayer2') self.scrolltext = None self.displaytext = '' self.is_playing = False self.scroll_timer = None self.scroll_counter = None def update(self, interface_name, changed_properties, invalidated_properties): """http://specifications.freedesktop.org/mpris-spec/latest/Track_List_Interface.html#Mapping:Metadata_Map""" if not self.configured: return True olddisplaytext = self.displaytext self.displaytext = '' metadata = None playbackstatus = None metadata = changed_properties.get('Metadata') playbackstatus = changed_properties.get('PlaybackStatus') if metadata: self.is_playing = True self.displaytext = ' - '.join([metadata.get(x) if isinstance(metadata.get(x), dbus.String) else ' + '.join([y for y in metadata.get(x) if isinstance(y, dbus.String)]) for x in self.display_metadata if metadata.get(x)]) self.displaytext.replace('\n', '') if playbackstatus: if playbackstatus == 'Paused' and self.stop_pause_text is not None: self.is_playing = False self.displaytext = self.stop_pause_text elif playbackstatus == 'Paused' and olddisplaytext: self.is_playing = False self.displaytext = 'Paused: {}'.format(olddisplaytext) elif playbackstatus == 'Paused': self.is_playing = False self.displaytext = 'Paused' elif playbackstatus == 'Playing' and not self.displaytext and \ olddisplaytext: self.is_playing = True self.displaytext = olddisplaytext.replace('Paused: ', '') elif playbackstatus == 'Playing' and not self.displaytext and \ not olddisplaytext: self.is_playing = True self.displaytext = 'No metadata for current track' elif playbackstatus == 'Playing' and self.displaytext: # Players might send more than one "Playing" message. pass else: self.is_playing = False self.displaytext = '' if self.scroll_chars and self.scroll_interval: if(self.scroll_timer): self.scroll_timer.cancel() self.scrolltext = self.displaytext self.scroll_counter = self.scroll_wait_intervals self.scroll_timer = self.timeout_add(self.scroll_interval, self.scroll_text) return if self.text != self.displaytext: self.text = self.displaytext self.bar.draw() def scroll_text(self): if self.text != self.scrolltext[:self.scroll_chars]: self.text = self.scrolltext[:self.scroll_chars] self.bar.draw() if self.scroll_counter: self.scroll_counter -= 1 if self.scroll_counter: self.timeout_add(self.scroll_interval, self.scroll_text) return if len(self.scrolltext) >= self.scroll_chars: self.scrolltext = self.scrolltext[1:] if len(self.scrolltext) == self.scroll_chars: self.scroll_counter += self.scroll_wait_intervals self.timeout_add(self.scroll_interval, self.scroll_text) return self.text = '' self.bar.draw() def cmd_info(self): """What's the current state of the widget?""" return dict( displaytext=self.displaytext, isplaying=self.is_playing, ) qtile-0.10.7/libqtile/widget/mpriswidget.py000066400000000000000000000143021305063162100207120ustar00rootroot00000000000000# Copyright (c) 2011 Mounier Florian # Copyright (c) 2011, 2014 Tycho Andersen # Copyright (c) 2012-2013 Craig Barnes # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 Sean Vig # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import dbus from dbus.mainloop.glib import DBusGMainLoop from libqtile.log_utils import logger from . import base class Mpris(base._TextBox): """MPRIS player widget A widget which displays the current track/artist of your favorite MPRIS player. It should work with all players which implement a reasonably correct version of MPRIS, though I have only tested it with clementine. """ orientations = base.ORIENTATION_HORIZONTAL defaults = [ ('name', 'clementine', 'Name of the widget'), ('objname', 'org.mpris.clementine', 'DBUS object to connect to'), ('stop_pause_text', 'Stopped', "Optional text to display when in the stopped/paused state"), ] def __init__(self, **config): base._TextBox.__init__(self, " ", **config) self.add_defaults(Mpris.defaults) self.dbus_loop = None self.connected = False def _configure(self, qtile, bar): base._TextBox._configure(self, qtile, bar) # we don't need to reconnect all the dbus stuff if we already # connected it. if self.dbus_loop is not None: return # we need a main loop to get event signals # we just piggyback on qtile's main loop self.dbus_loop = DBusGMainLoop() self.bus = dbus.SessionBus(mainloop=self.dbus_loop) # watch for our player to start up deebus = self.bus.get_object( 'org.freedesktop.DBus', '/org/freedesktop/DBus' ) deebus.connect_to_signal( "NameOwnerChanged", self.handle_name_owner_change ) # try to connect for grins self._connect() def _connect(self): """Try to connect to the player if it exists""" try: self.player = self.bus.get_object(self.objname, '/Player') self.iface = dbus.Interface( self.player, dbus_interface='org.freedesktop.MediaPlayer' ) # See: http://xmms2.org/wiki/MPRIS for info on signals # and what they mean. self.iface.connect_to_signal( "TrackChange", self.handle_track_change ) self.iface.connect_to_signal( "StatusChange", self.handle_status_change ) self.connected = True except dbus.exceptions.DBusException: logger.exception("exception initializing mpris") self.connected = False def handle_track_change(self, metadata): self.update() def handle_status_change(self, *args): self.update() def handle_name_owner_change(self, name, old_owner, new_owner): if name == self.objname: if old_owner == '': # Our player started, so connect to it self._connect() elif new_owner == '': # It disconnected :-( self.connected = False self.update() def ensure_connected(f): """Tries to connect to the player It *should* be successful if the player is alive """ def wrapper(*args, **kwargs): self = args[0] try: self.iface.GetMetadata() except (dbus.exceptions.DBusException, AttributeError): # except AttributeError because # self.iface won't exist if we haven't # _connect()ed yet self._connect() return f(*args, **kwargs) return wrapper @ensure_connected def update(self): self.qtile.call_soon_threadsafe(self.real_update) @ensure_connected def real_update(self): if not self.configured: playing = 'Not configured' if not self.connected: playing = 'Not Connected' elif not self.is_playing(): playing = self.stop_pause_text else: try: metadata = self.iface.GetMetadata() # TODO: Make this configurable? playing = metadata["title"] + ' - ' + metadata["artist"] except dbus.exceptions.DBusException: self.connected = False playing = '' if playing != self.text: self.text = playing self.bar.draw() @ensure_connected def is_playing(self): """Checks status of the player Returns true if we are connected to the player and it is playing something, false otherwise """ if self.connected: (playing, random, repeat, stop_after_last) = self.iface.GetStatus() return playing == 0 else: return False def cmd_info(self): """What's the current state of the widget?""" return dict( connected=self.connected, nowplaying=self.text, isplaying=self.is_playing(), ) def cmd_update(self): """Force the widget to update. Mostly used for testing""" self.update() qtile-0.10.7/libqtile/widget/net.py000066400000000000000000000071661305063162100171540ustar00rootroot00000000000000# Copyright (c) 2014 Rock Neurotiko # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import division from libqtile.log_utils import logger from . import base class Net(base.ThreadedPollText): """Displays interface down and up speed""" orientations = base.ORIENTATION_HORIZONTAL defaults = [ ('interface', 'wlan0', 'The interface to monitor'), ('update_interval', 1, 'The update interval.'), ] def __init__(self, **config): base.ThreadedPollText.__init__(self, **config) self.add_defaults(Net.defaults) self.interfaces = self.get_stats() def convert_b(self, b): # Here we round to 1000 instead of 1024 # because of round things letter = 'B' # b is a float, so don't use integer division if int(b / 1000) > 0: b /= 1000.0 letter = 'k' if int(b / 1000) > 0: b /= 1000.0 letter = 'M' if int(b / 1000) > 0: b /= 1000.0 letter = 'G' # I hope no one have more than 999 GB/s return b, letter def get_stats(self): lines = [] # type: List[str] with open('/proc/net/dev', 'r') as f: lines = f.readlines()[2:] interfaces = {} for s in lines: int_s = s.split() name = int_s[0][:-1] down = float(int_s[1]) up = float(int_s[-8]) interfaces[name] = {'down': down, 'up': up} return interfaces def _format(self, down, up): down = "%0.2f" % down up = "%0.2f" % up if len(down) > 5: down = down[:5] if len(up) > 5: up = up[:5] down = " " * (5 - len(down)) + down up = " " * (5 - len(up)) + up return down, up def poll(self): try: new_int = self.get_stats() down = new_int[self.interface]['down'] - \ self.interfaces[self.interface]['down'] up = new_int[self.interface]['up'] - \ self.interfaces[self.interface]['up'] down = down / self.update_interval up = up / self.update_interval down, down_letter = self.convert_b(down) up, up_letter = self.convert_b(up) down, up = self._format(down, up) str_base = u"%s%s \u2193\u2191 %s%s" self.interfaces = new_int return str_base % (down, down_letter, up, up_letter) except Exception: logger.error('%s: Probably your wlan device is switched off or otherwise not present in your system.', self.__class__.__name__) qtile-0.10.7/libqtile/widget/notify.py000066400000000000000000000113661305063162100176730ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2011 Florian Mounier # Copyright (c) 2011 Mounier Florian # Copyright (c) 2012 roger # Copyright (c) 2012-2014 Tycho Andersen # Copyright (c) 2012-2013 Craig Barnes # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Adi Sieker # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from . import base from .. import bar, utils, pangocffi from libqtile.notify import notifier from os import path class Notify(base._TextBox): """A notify widget""" orientations = base.ORIENTATION_HORIZONTAL defaults = [ ("foreground_urgent", "ff0000", "Foreground urgent priority colour"), ("foreground_low", "dddddd", "Foreground low priority colour"), ( "default_timeout", None, "Default timeout (seconds) for notifications" ), ("audiofile", None, "Audiofile played during notifications"), ] def __init__(self, width=bar.CALCULATED, **config): base._TextBox.__init__(self, "", width, **config) self.add_defaults(Notify.defaults) notifier.register(self.update) self.current_id = 0 def _configure(self, qtile, bar): base._TextBox._configure(self, qtile, bar) self.layout = self.drawer.textlayout( self.text, self.foreground, self.font, self.fontsize, self.fontshadow, markup=True ) def set_notif_text(self, notif): self.text = pangocffi.markup_escape_text(notif.summary) urgency = notif.hints.get('urgency', 1) if urgency != 1: self.text = '%s' % ( utils.hex( self.foreground_urgent if urgency == 2 else self.foreground_low ), self.text ) if notif.body: self.text = '%s - %s' % ( self.text, pangocffi.markup_escape_text(notif.body) ) if self.audiofile and path.exists(self.audiofile): self.qtile.cmd_spawn("aplay -q '%s'" % self.audiofile) def update(self, notif): self.qtile.call_soon_threadsafe(self.real_update, notif) def real_update(self, notif): self.set_notif_text(notif) self.current_id = notif.id - 1 if notif.timeout and notif.timeout > 0: self.timeout_add(notif.timeout / 1000, self.clear) elif self.default_timeout: self.timeout_add(self.default_timeout, self.clear) self.bar.draw() return True def display(self): self.set_notif_text(notifier.notifications[self.current_id]) self.bar.draw() def clear(self): self.text = '' self.current_id = len(notifier.notifications) - 1 self.bar.draw() def prev(self): if self.current_id > 0: self.current_id -= 1 self.display() def next(self): if self.current_id < len(notifier.notifications) - 1: self.current_id += 1 self.display() def button_press(self, x, y, button): if button == 1: self.clear() elif button == 4: self.prev() elif button == 5: self.next() def cmd_display(self): """Display the notifcication""" self.display() def cmd_clear(self): """Clear the notification""" self.clear() def cmd_toggle(self): """Toggle showing/clearing the notification""" if self.text == '': self.display() else: self.clear() def cmd_prev(self): """Show previous notification""" self.prev() def cmd_next(self): """Show next notification""" self.next() qtile-0.10.7/libqtile/widget/pacman.py000066400000000000000000000036371305063162100176240ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright (C) 2012, Maximilian Köhl # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from . import base import subprocess class Pacman(base.ThreadedPollText): """Shows number of available updates Needs the pacman package manager installed. So will only work in Arch Linux installation. """ orientations = base.ORIENTATION_HORIZONTAL defaults = [ ('unavailable', 'ffffff', 'Unavailable Color - no updates.'), ('execute', None, 'Command to execute on click'), ('update_interval', 60, "The update interval."), ] def __init__(self, **config): base.deprecated("Pacman is deprecated, please use CheckUpdates") base.ThreadedPollText.__init__(self, **config) self.add_defaults(Pacman.defaults) def draw(self): if self.text == '0': self.layout.colour = self.unavailable else: self.layout.colour = self.foreground base.ThreadedPollText.draw(self) def poll(self): pacman = self.call_process(['checkupdates']) return str(len(pacman.splitlines())) def button_press(self, x, y, button): base.ThreadedPollText.button_press(self, x, y, button) if button == 1 and self.execute is not None: subprocess.Popen([self.execute], shell=True) qtile-0.10.7/libqtile/widget/prompt.py000066400000000000000000000604501305063162100177020ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2010-2011 Aldo Cortesi # Copyright (c) 2010 Philip Kranz # Copyright (c) 2011 Mounier Florian # Copyright (c) 2011 Paul Colomiets # Copyright (c) 2011-2012 roger # Copyright (c) 2011-2012, 2014 Tycho Andersen # Copyright (c) 2012 Dustin Lacewell # Copyright (c) 2012 Laurie Clark-Michalek # Copyright (c) 2012-2014 Craig Barnes # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sean Vig # Copyright (C) 2015, Juan Riquelme González # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import glob import os import pickle import string from collections import OrderedDict, deque from libqtile.log_utils import logger from . import base from .. import bar, command, hook, pangocffi, utils, xcbq, xkeysyms class NullCompleter(object): def __init__(self, qtile): self.qtile = qtile self.thisfinal = "" def actual(self): return self.thisfinal def reset(self): pass def complete(self, txt): return txt class FileCompleter(object): def __init__(self, qtile, _testing=False): self._testing = _testing self.qtile = qtile self.thisfinal = None self.reset() def actual(self): return self.thisfinal def reset(self): self.lookup = None def complete(self, txt): """Returns the next completion for txt, or None if there is no completion""" if not self.lookup: self.lookup = [] if txt == "" or txt[0] not in "~/": txt = "~/" + txt path = os.path.expanduser(txt) if os.path.isdir(path): files = glob.glob(os.path.join(path, "*")) prefix = txt else: files = glob.glob(path + "*") prefix = os.path.dirname(txt) prefix = prefix.rstrip("/") or "/" for f in files: display = os.path.join(prefix, os.path.basename(f)) if os.path.isdir(f): display += "/" self.lookup.append((display, f)) self.lookup.sort() self.offset = -1 self.lookup.append((txt, txt)) self.offset += 1 if self.offset >= len(self.lookup): self.offset = 0 ret = self.lookup[self.offset] self.thisfinal = ret[1] return ret[0] class QshCompleter(object): def __init__(self, qtile): self.qtile = qtile self.client = command.CommandRoot(self.qtile) self.thisfinal = None self.reset() def actual(self): return self.thisfinal def reset(self): self.lookup = None self.path = '' self.offset = -1 def complete(self, txt): txt = txt.lower() if not self.lookup: self.lookup = [] path = txt.split('.')[:-1] self.path = '.'.join(path) term = txt.split('.')[-1] if len(self.path) > 0: self.path += '.' contains_cmd = 'self.client.%s_contains' % self.path try: contains = eval(contains_cmd) except AttributeError: contains = [] for obj in contains: if obj.lower().startswith(term): self.lookup.append((obj, obj)) commands_cmd = 'self.client.%scommands()' % self.path try: commands = eval(commands_cmd) except (command.CommandError, AttributeError): commands = [] for cmd in commands: if cmd.lower().startswith(term): self.lookup.append((cmd + '()', cmd + '()')) self.offset = -1 self.lookup.append((term, term)) self.offset += 1 if self.offset >= len(self.lookup): self.offset = 0 ret = self.lookup[self.offset] self.thisfinal = self.path + ret[0] return self.path + ret[0] class GroupCompleter(object): def __init__(self, qtile): self.qtile = qtile self.thisfinal = None self.lookup = None self.offset = None def actual(self): """Returns the current actual value""" return self.thisfinal def reset(self): self.lookup = None self.offset = -1 def complete(self, txt): """Returns the next completion for txt, or None if there is no completion""" txt = txt.lower() if not self.lookup: self.lookup = [] for group in self.qtile.groupMap.keys(): if group.lower().startswith(txt): self.lookup.append((group, group)) self.lookup.sort() self.offset = -1 self.lookup.append((txt, txt)) self.offset += 1 if self.offset >= len(self.lookup): self.offset = 0 ret = self.lookup[self.offset] self.thisfinal = ret[1] return ret[0] class WindowCompleter(object): def __init__(self, qtile): self.qtile = qtile self.thisfinal = None self.lookup = None self.offset = None def actual(self): """Returns the current actual value""" return self.thisfinal def reset(self): self.lookup = None self.offset = -1 def complete(self, txt): """Returns the next completion for txt, or None if there is no completion""" if not self.lookup: self.lookup = [] for wid, window in self.qtile.windowMap.items(): if window.group and window.name.lower().startswith(txt): self.lookup.append((window.name, wid)) self.lookup.sort() self.offset = -1 self.lookup.append((txt, txt)) self.offset += 1 if self.offset >= len(self.lookup): self.offset = 0 ret = self.lookup[self.offset] self.thisfinal = ret[1] return ret[0] class CommandCompleter(object): """ Parameters ========== _testing : disables reloading of the lookup table to make testing possible. """ DEFAULTPATH = "/bin:/usr/bin:/usr/local/bin" def __init__(self, qtile, _testing=False): self.lookup = None self.offset = -1 self.thisfinal = None self._testing = _testing def actual(self): """Returns the current actual value""" return self.thisfinal def executable(self, fpath): return os.access(fpath, os.X_OK) def reset(self): self.lookup = None self.offset = -1 def complete(self, txt): """Returns the next completion for txt, or None if there is no completion""" if not self.lookup: if not self._testing: # Lookup is a set of (display value, actual value) tuples. self.lookup = [] if txt and txt[0] in "~/": path = os.path.expanduser(txt) if os.path.isdir(path): files = glob.glob(os.path.join(path, "*")) prefix = txt else: files = glob.glob(path + "*") prefix = os.path.dirname(txt) prefix = prefix.rstrip("/") or "/" for f in files: if self.executable(f): display = os.path.join(prefix, os.path.basename(f)) if os.path.isdir(f): display += "/" self.lookup.append((display, f)) else: dirs = os.environ.get("PATH", self.DEFAULTPATH).split(":") for d in dirs: try: d = os.path.expanduser(d) for cmd in glob.iglob(os.path.join(d, "%s*" % txt)): if self.executable(cmd): self.lookup.append( ( os.path.basename(cmd), cmd ), ) except OSError: pass self.lookup.sort() self.offset = -1 self.lookup.append((txt, txt)) self.offset += 1 if self.offset >= len(self.lookup): self.offset = 0 ret = self.lookup[self.offset] self.thisfinal = ret[1] return ret[0] class Prompt(base._TextBox): """A widget that prompts for user input Input should be started using the ``.startInput()`` method on this class. """ completers = { "file": FileCompleter, "qshell": QshCompleter, "cmd": CommandCompleter, "group": GroupCompleter, "window": WindowCompleter, None: NullCompleter } orientations = base.ORIENTATION_HORIZONTAL defaults = [("cursor", True, "Show a cursor"), ("cursorblink", 0.5, "Cursor blink rate. 0 to disable."), ("cursor_color", "bef098", "Color for the cursor and text over it."), ("prompt", "{prompt}: ", "Text displayed at the prompt"), ("record_history", True, "Keep a record of executed commands"), ("max_history", 100, "Commands to keep in history. 0 for no limit."), ("ignore_dups_history", False, "Don't store duplicates in history"), ("bell_style", "audible", "Alert at the begin/end of the command history. " + "Possible values: 'audible', 'visual' and None."), ("visual_bell_color", "ff0000", "Color for the visual bell (changes prompt background)."), ("visual_bell_time", 0.2, "Visual bell duration (in seconds).")] def __init__(self, name="prompt", **config): base._TextBox.__init__(self, "", bar.CALCULATED, **config) self.add_defaults(Prompt.defaults) self.name = name self.active = False self.completer = None # Define key handlers (action to do when hit an specific key) self.keyhandlers = { xkeysyms.keysyms['Tab']: self._trigger_complete, xkeysyms.keysyms['BackSpace']: self._delete_char(), xkeysyms.keysyms['Delete']: self._delete_char(False), xkeysyms.keysyms['KP_Delete']: self._delete_char(False), xkeysyms.keysyms['Escape']: self._unfocus, xkeysyms.keysyms['Return']: self._send_cmd, xkeysyms.keysyms['KP_Enter']: self._send_cmd, xkeysyms.keysyms['Up']: self._get_prev_cmd, xkeysyms.keysyms['KP_Up']: self._get_prev_cmd, xkeysyms.keysyms['Down']: self._get_next_cmd, xkeysyms.keysyms['KP_Down']: self._get_next_cmd, xkeysyms.keysyms['Left']: self._move_cursor(), xkeysyms.keysyms['KP_Left']: self._move_cursor(), xkeysyms.keysyms['Right']: self._move_cursor("right"), xkeysyms.keysyms['KP_Right']: self._move_cursor("right"), } printables = [int(hex(x), 16) for x in range(127)] printables = {x: self._write_char for x in printables if chr(x) in string.printable} self.keyhandlers.update(printables) if self.bell_style == "visual": self.original_background = self.background # If history record is on, get saved history or create history record if self.record_history: self.history_path = os.path.join(utils.get_cache_dir(), 'prompt_history') if os.path.exists(self.history_path): with open(self.history_path, 'rb') as f: try: self.history = pickle.load(f) if self.ignore_dups_history: self._dedup_history() except: # unfortunately, pickle doesn't wrap its errors, so we # can't detect what's a pickle error and what's not. logger.exception("failed to load prompt history") self.history = {x: deque(maxlen=self.max_history) for x in self.completers if x} if self.max_history != \ self.history[list(self.history)[0]].maxlen: self.history = {x: deque(self.history[x], self.max_history) for x in self.completers if x} else: self.history = {x: deque(maxlen=self.max_history) for x in self.completers if x} def _configure(self, qtile, bar): self.markup = True base._TextBox._configure(self, qtile, bar) def f(win): if self.active and not win == self.bar.window: self._unfocus() hook.subscribe.client_focus(f) def startInput(self, prompt, callback, complete=None, strict_completer=False): """Run the prompt Displays a prompt and starts to take one line of keyboard input from the user. When done, calls the callback with the input string as argument. If history record is enabled, also allows to browse between previous commands with ↑ and ↓, and execute them (untouched or modified). When history is exhausted, fires an alert. It tries to mimic, in some way, the shell behavior. Parameters ========== complete : Tab-completion. Can be None, "cmd", "file", "group", "qshell" or "window". prompt : text displayed at the prompt, e.g. "spawn: " callback : function to call with returned value. complete : completer to use. strict_completer : When True the return value wil be the exact completer result where available. """ if self.cursor and self.cursorblink and not self.active: self.timeout_add(self.cursorblink, self._blink) self.display = self.prompt.format(prompt=prompt) self.display = pangocffi.markup_escape_text(self.display) self.active = True self.userInput = "" self.archivedInput = "" self.show_cursor = self.cursor self.cursor_position = 0 self.callback = callback self.completer = self.completers[complete](self.qtile) self.strict_completer = strict_completer self._update() self.bar.widget_grab_keyboard(self) if self.record_history: self.completer_history = self.history[complete] self.position = len(self.completer_history) def calculate_length(self): if self.text: width = min( self.layout.width, self.bar.width ) + self.actual_padding * 2 return width else: return 0 def _blink(self): self.show_cursor = not self.show_cursor self._update() if self.active: self.timeout_add(self.cursorblink, self._blink) def _highlight_text(self, text): color = utils.hex(self.cursor_color) text = '{1}'.format(color, text) if self.show_cursor: text = '{}'.format(text) return text def _update(self): if self.active: self.text = self.archivedInput or self.userInput cursor = pangocffi.markup_escape_text(" ") if self.cursor_position < len(self.text): txt1 = self.text[:self.cursor_position] txt2 = self.text[self.cursor_position] txt3 = self.text[self.cursor_position + 1:] for text in (txt1, txt2, txt3): text = pangocffi.markup_escape_text(text) txt2 = self._highlight_text(txt2) self.text = "{0}{1}{2}{3}".format(txt1, txt2, txt3, cursor) else: self.text = pangocffi.markup_escape_text(self.text) self.text += self._highlight_text(cursor) self.text = self.display + self.text else: self.text = "" self.bar.draw() def _trigger_complete(self): # Trigger the auto completion in user input self.userInput = self.completer.complete(self.userInput) self.cursor_position = len(self.userInput) def _history_to_input(self): # Move actual command (when exploring history) to user input and update # history position (right after the end) if self.archivedInput: self.userInput = self.archivedInput self.archivedInput = "" self.position = len(self.completer_history) def _insert_before_cursor(self, charcode): # Insert a character (given their charcode) in input, before the cursor txt1 = self.userInput[:self.cursor_position] txt2 = self.userInput[self.cursor_position:] self.userInput = txt1 + chr(charcode) + txt2 self.cursor_position += 1 def _delete_char(self, backspace=True): # Return a function that deletes character from the input text. # If backspace is True, function will emulate backspace, else Delete. def f(): self._history_to_input() step = -1 if backspace else 0 if not backspace and self.cursor_position == len(self.userInput): self._alert() elif len(self.userInput) > 0 and self.cursor_position + step > -1: txt1 = self.userInput[:self.cursor_position + step] txt2 = self.userInput[self.cursor_position + step + 1:] self.userInput = txt1 + txt2 if step: self.cursor_position += step else: self._alert() return f def _write_char(self): # Add pressed (legal) char key to user input. # No LookupString in XCB... oh, the shame! Unicode users beware! self._history_to_input() self._insert_before_cursor(self.key) def _unfocus(self): # Remove focus from the widget self.active = False self._update() self.bar.widget_ungrab_keyboard() def _send_cmd(self): # Send the prompted text for execution self._unfocus() if self.strict_completer: self.userInput = self.actual_value or self.userInput del self.actual_value self._history_to_input() if self.userInput: # If history record is activated, also save command in history if self.record_history: # ensure no dups in history if self.ignore_dups_history and (self.userInput in self.completer_history): self.completer_history.remove(self.userInput) self.position -= 1 self.completer_history.append(self.userInput) if self.position < self.max_history: self.position += 1 with open(self.history_path, mode='wb') as f: pickle.dump(self.history, f, protocol=2) self.callback(self.userInput) def _alert(self): # Fire an alert (audible or visual), if bell style is not None. if self.bell_style == "audible": self.qtile.conn.conn.core.Bell(0) elif self.bell_style == "visual": self.background = self.visual_bell_color self.timeout_add(self.visual_bell_time, self._stop_visual_alert) def _stop_visual_alert(self): self.background = self.original_background self._update() def _get_prev_cmd(self): # Get the previous command in history. # If there isn't more previous commands, ring system bell if self.record_history: if not self.position: self._alert() else: self.position -= 1 self.archivedInput = self.completer_history[self.position] self.cursor_position = len(self.archivedInput) def _get_next_cmd(self): # Get the next command in history. # If the last command was already reached, ring system bell. if self.record_history: if self.position == len(self.completer_history): self._alert() elif self.position < len(self.completer_history): self.position += 1 if self.position == len(self.completer_history): self.archivedInput = "" else: self.archivedInput = self.completer_history[self.position] self.cursor_position = len(self.archivedInput) def _cursor_to_left(self): # Move cursor to left, if possible if self.cursor_position: self.cursor_position -= 1 else: self._alert() def _cursor_to_right(self): # move cursor to right, if possible command = self.archivedInput or self.userInput if self.cursor_position < len(command): self.cursor_position += 1 else: self._alert() def _move_cursor(self, direction="left"): # Move the cursor to left or right, according to direction if direction == "left": return self._cursor_to_left elif direction == "right": return self._cursor_to_right def _get_keyhandler(self, k): # Return the action (a function) to do according the pressed key (k). self.key = k if k in self.keyhandlers: if k != xkeysyms.keysyms['Tab']: self.actual_value = self.completer.actual() self.completer.reset() return self.keyhandlers[k] def handle_KeyPress(self, e): """KeyPress handler for the minibuffer. Currently only supports ASCII characters. """ mask = xcbq.ModMasks["shift"] | xcbq.ModMasks["lock"] state = 1 if e.state & mask else 0 keysym = self.qtile.conn.code_to_syms[e.detail][state] handle_key = self._get_keyhandler(keysym) if handle_key: handle_key() del self.key self._update() def cmd_fake_keypress(self, key): class Dummy(object): pass d = Dummy() keysym = xcbq.keysyms[key] d.detail = self.qtile.conn.keysym_to_keycode(keysym) d.state = 0 self.handle_KeyPress(d) def cmd_info(self): """Returns a dictionary of info for this object""" return dict( name=self.name, width=self.width, text=self.text, active=self.active, ) def _dedup_history(self): """Filter the history deque, clearing all duplicate values.""" self.history = {x: self._dedup_deque(self.history[x]) for x in self.completers if x} def _dedup_deque(self, dq): return deque(_LastUpdatedOrderedDict.fromkeys(dq)) class _LastUpdatedOrderedDict(OrderedDict): """Store items in the order the keys were last added.""" def __setitem__(self, key, value): if key in self: del self[key] OrderedDict.__setitem__(self, key, value) qtile-0.10.7/libqtile/widget/sensors.py000066400000000000000000000114201305063162100200460ustar00rootroot00000000000000# -*- coding:utf-8 -*- # Copyright (c) 2012 TiN # Copyright (c) 2012, 2014 Tycho Andersen # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014-2015 Sean Vig # Copyright (c) 2014 Adi Sieker # Copyright (c) 2014 Foster McLane # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import re from six import PY2 from . import base from ..utils import UnixCommandNotFound, catch_exception_and_warn from libqtile.log_utils import logger class ThermalSensor(base.InLoopPollText): """Widget to display temperature sensor information For using the thermal sensor widget you need to have lm-sensors installed. You can get a list of the tag_sensors executing "sensors" in your terminal. Then you can choose which you want, otherwise it will display the first available. """ orientations = base.ORIENTATION_HORIZONTAL defaults = [ ('metric', True, 'True to use metric/C, False to use imperial/F'), ('show_tag', False, 'Show tag sensor'), ('update_interval', 2, 'Update interval in seconds'), ('tag_sensor', None, 'Tag of the temperature sensor. For example: "temp1" or "Core 0"'), ( 'threshold', 70, 'If the current temperature value is above, ' 'then change to foreground_alert colour' ), ('foreground_alert', 'ff0000', 'Foreground colour alert'), ] def __init__(self, **config): base.InLoopPollText.__init__(self, **config) self.add_defaults(ThermalSensor.defaults) self.sensors_temp = re.compile( (r"\n([\w ]+):" # Sensor tag name r"\s+[+|-]" # temp signed r"(\d+\.\d+)" # temp value u"({degrees}" # degree symbol match u"[C|F])" # Celsius or Fahrenheit ).format(degrees=u"\xc2\xb0" if PY2 else u"\xb0"), re.UNICODE | re.VERBOSE ) self.value_temp = re.compile("\d+\.\d+") temp_values = self.get_temp_sensors() self.foreground_normal = self.foreground if temp_values is None: self.data = "sensors command not found" elif len(temp_values) == 0: self.data = "Temperature sensors not found" elif self.tag_sensor is None: for k in temp_values: self.tag_sensor = k break @catch_exception_and_warn(warning=UnixCommandNotFound, excepts=OSError) def get_temp_sensors(self): """calls the unix `sensors` command with `-f` flag if user has specified that the output should be read in Fahrenheit. """ command = ["sensors", ] if not self.metric: command.append("-f") sensors_out = self.call_process(command) return self._format_sensors_output(sensors_out) def _format_sensors_output(self, sensors_out): """formats output of unix `sensors` command into a dict of {: (, ), ..etc..} """ temperature_values = {} logger.info(self.sensors_temp.findall(sensors_out)) for name, temp, symbol in self.sensors_temp.findall(sensors_out): name = name.strip() temperature_values[name] = temp, symbol return temperature_values def poll(self): temp_values = self.get_temp_sensors() if temp_values is None: return False text = "" if self.show_tag and self.tag_sensor is not None: text = self.tag_sensor + ": " text += "".join(temp_values.get(self.tag_sensor, ['N/A'])) temp_value = float(temp_values.get(self.tag_sensor, [0])[0]) if temp_value > self.threshold: self.layout.colour = self.foreground_alert else: self.layout.colour = self.foreground_normal return text qtile-0.10.7/libqtile/widget/sep.py000066400000000000000000000063771305063162100171600ustar00rootroot00000000000000# Copyright (c) 2010 Aldo Cortesi # Copyright (c) 2011 Mounier Florian # Copyright (c) 2012, 2015 Tycho Andersen # Copyright (c) 2012 Craig Barnes # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 Sean Vig # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from . import base class Sep(base._Widget): """A visible widget separator""" orientations = base.ORIENTATION_BOTH defaults = [ ("padding", 2, "Padding on either side of separator."), ("linewidth", 1, "Width of separator line."), ("foreground", "888888", "Separator line colour."), ( "size_percent", 80, "Size as a percentage of bar size (0-100)." ), ] def __init__(self, height_percent=None, **config): # 'height_percent' was replaced by 'size_percent' since the widget can # be installed in vertical bars if height_percent is not None: base.deprecated('height_percent kwarg or positional argument is ' 'deprecated. Please use size_percent.') config["size_percent"] = height_percent length = config.get("padding", 2) * 2 + config.get("linewidth", 1) base._Widget.__init__(self, length, **config) self.add_defaults(Sep.defaults) self.length = self.padding + self.linewidth def draw(self): self.drawer.clear(self.background or self.bar.background) if self.bar.horizontal: margin_top = (self.bar.height / float(100) * (100 - self.size_percent)) / 2.0 self.drawer.draw_vbar( self.foreground, float(self.length) / 2, margin_top, self.bar.height - margin_top, linewidth=self.linewidth ) self.drawer.draw(offsetx=self.offset, width=self.length) else: margin_left = (self.bar.width / float(100) * (100 - self.size_percent)) / 2.0 self.drawer.draw_hbar( self.foreground, margin_left, self.bar.width - margin_left, float(self.length) / 2, linewidth=self.linewidth ) self.drawer.draw(offsety=self.offset, height=self.length) qtile-0.10.7/libqtile/widget/she.py000066400000000000000000000042051305063162100171340ustar00rootroot00000000000000# Copyright (c) 2012, 2014 Tycho Andersen # Copyright (c) 2012, 2014 Craig Barnes # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from libqtile.widget import base class She(base.InLoopPollText): """Widget to display the Super Hybrid Engine status Can display either the mode or CPU speed on eeepc computers. """ orientations = base.ORIENTATION_HORIZONTAL defaults = [ ('device', '/sys/devices/platform/eeepc/cpufv', 'sys path to cpufv'), ('format', 'speed', 'Type of info to display "speed" or "name"'), ('update_interval', 0.5, 'Update Time in seconds.'), ] def __init__(self, **config): base.InLoopPollText.__init__(self, **config) self.add_defaults(She.defaults) self.modes = { '0x300': {'name': 'Performance', 'speed': '1.6GHz'}, '0x301': {'name': 'Normal', 'speed': '1.2GHz'}, '0x302': {'name': 'PoswerSave', 'speed': '800MHz'} } def poll(self): with open(self.device) as f: mode = f.read().strip() if mode in self.modes: return self.modes[mode][self.format] else: return mode qtile-0.10.7/libqtile/widget/spacer.py000066400000000000000000000045761305063162100176450ustar00rootroot00000000000000# Copyright (c) 2008, 2010 Aldo Cortesi # Copyright (c) 2011 Mounier Florian # Copyright (c) 2012 Tim Neumann # Copyright (c) 2012 Craig Barnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Adi Sieker # Copyright (c) 2014 Tycho Andersen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from .. import bar from . import base class Spacer(base._Widget): """Just an empty space on the bar Often used with length equal to bar.STRETCH to push bar widgets to the right or bottom edge of the screen. Parameters ========== length : Length of the widget. Can be either ``bar.STRETCH`` or a length in pixels. width : DEPRECATED, same as ``length``. """ orientations = base.ORIENTATION_BOTH def __init__(self, length=bar.STRETCH, width=None): """ """ # 'width' was replaced by 'length' since the widget can be installed in # vertical bars if width is not None: base.deprecated('width kwarg or positional argument is ' 'deprecated. Please use length.') length = width base._Widget.__init__(self, length) def draw(self): self.drawer.clear(self.bar.background) if self.bar.horizontal: self.drawer.draw(offsetx=self.offset, width=self.length) else: self.drawer.draw(offsety=self.offset, height=self.length) qtile-0.10.7/libqtile/widget/systray.py000066400000000000000000000163661305063162100201060ustar00rootroot00000000000000# Copyright (c) 2010 Aldo Cortesi # Copyright (c) 2010-2011 dequis # Copyright (c) 2010, 2012 roger # Copyright (c) 2011 Mounier Florian # Copyright (c) 2011-2012, 2014 Tycho Andersen # Copyright (c) 2012 dmpayton # Copyright (c) 2012-2013 Craig Barnes # Copyright (c) 2013 hbc # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 Sean Vig # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import division from .. import bar, xcbq, window from . import base import xcffib from xcffib.xproto import (ClientMessageEvent, ClientMessageData, EventMask, SetMode) XEMBED_PROTOCOL_VERSION = 0 class Icon(window._Window): _windowMask = EventMask.StructureNotify | \ EventMask.PropertyChange | \ EventMask.Exposure def __init__(self, win, qtile, systray): window._Window.__init__(self, win, qtile) self.systray = systray self.update_size() def update_size(self): icon_size = self.systray.icon_size self.updateHints() try: width = self.hints["min_width"] height = self.hints["min_height"] except KeyError: width = icon_size height = icon_size if height > icon_size: width = width * icon_size // height height = icon_size if height <= 0: width = icon_size height = icon_size self.width = width self.height = height return False def handle_PropertyNotify(self, e): name = self.qtile.conn.atoms.get_name(e.atom) if name == "_XEMBED_INFO": info = self.window.get_property('_XEMBED_INFO', unpack=int) if info and info[1]: self.systray.bar.draw() return False def handle_DestroyNotify(self, event): wid = event.window del(self.qtile.windowMap[wid]) del(self.systray.icons[wid]) self.systray.bar.draw() return False handle_UnmapNotify = handle_DestroyNotify class Systray(window._Window, base._Widget): """A widget that manages system tray""" _windowMask = EventMask.StructureNotify | \ EventMask.Exposure orientations = base.ORIENTATION_HORIZONTAL defaults = [ ('icon_size', 20, 'Icon width'), ('padding', 5, 'Padding between icons'), ] def __init__(self, **config): base._Widget.__init__(self, bar.CALCULATED, **config) self.add_defaults(Systray.defaults) self.icons = {} self.screen = 0 def calculate_length(self): width = sum(i.width for i in self.icons.values()) width += self.padding * len(self.icons) return width def _configure(self, qtile, bar): base._Widget._configure(self, qtile, bar) win = qtile.conn.create_window(-1, -1, 1, 1) window._Window.__init__(self, xcbq.Window(qtile.conn, win.wid), qtile) qtile.windowMap[win.wid] = self # Even when we have multiple "Screen"s, we are setting up as the system # tray on a particular X display, that is the screen we need to # reference in the atom if qtile.currentScreen: self.screen = qtile.currentScreen.index self.bar = bar atoms = qtile.conn.atoms qtile.conn.conn.core.SetSelectionOwner( win.wid, atoms['_NET_SYSTEM_TRAY_S{:d}'.format(self.screen)], xcffib.CurrentTime ) data = [ xcffib.CurrentTime, atoms['_NET_SYSTEM_TRAY_S{:d}'.format(self.screen)], win.wid, 0, 0 ] union = ClientMessageData.synthetic(data, "I" * 5) event = ClientMessageEvent.synthetic( format=32, window=qtile.root.wid, type=atoms['MANAGER'], data=union ) qtile.root.send_event(event, mask=EventMask.StructureNotify) def handle_ClientMessage(self, event): atoms = self.qtile.conn.atoms opcode = event.type data = event.data.data32 message = data[1] wid = data[2] conn = self.qtile.conn.conn parent = self.bar.window.window if opcode == atoms['_NET_SYSTEM_TRAY_OPCODE'] and message == 0: w = xcbq.Window(self.qtile.conn, wid) icon = Icon(w, self.qtile, self) self.icons[wid] = icon self.qtile.windowMap[wid] = icon conn.core.ChangeSaveSet(SetMode.Insert, wid) conn.core.ReparentWindow(wid, parent.wid, 0, 0) conn.flush() info = icon.window.get_property('_XEMBED_INFO', unpack=int) if not info: self.bar.draw() return False if info[1]: self.bar.draw() return False def draw(self): xoffset = self.padding self.drawer.clear(self.background or self.bar.background) self.drawer.draw(offsetx=self.offset, width=self.length) for pos, icon in enumerate(self.icons.values()): icon.window.set_attribute(backpixmap=self.drawer.pixmap) icon.place( self.offset + xoffset, self.bar.height // 2 - self.icon_size // 2, icon.width, self.icon_size, 0, None ) if icon.hidden: icon.unhide() data = [ self.qtile.conn.atoms["_XEMBED_EMBEDDED_NOTIFY"], xcffib.xproto.Time.CurrentTime, 0, self.bar.window.window.wid, XEMBED_PROTOCOL_VERSION ] u = xcffib.xproto.ClientMessageData.synthetic(data, "I" * 5) event = xcffib.xproto.ClientMessageEvent.synthetic( format=32, window=icon.window.wid, type=self.qtile.conn.atoms["_XEMBED"], data=u ) self.window.send_event(event) xoffset += icon.width + self.padding def finalize(self): base._Widget.finalize(self) atoms = self.qtile.conn.atoms self.qtile.conn.conn.core.SetSelectionOwner( 0, atoms['_NET_SYSTEM_TRAY_S{:d}'.format(self.screen)], xcffib.CurrentTime, ) self.hide() qtile-0.10.7/libqtile/widget/tasklist.py000066400000000000000000000217531305063162100202220ustar00rootroot00000000000000# Copyright (c) 2012-2014 roger # Copyright (c) 2012-2015 Tycho Andersen # Copyright (c) 2013 dequis # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2013 Craig Barnes # Copyright (c) 2014 Sean Vig # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import division import cairocffi from .. import bar, hook from . import base class TaskList(base._Widget, base.PaddingMixin, base.MarginMixin): """Displays the icon and name of each window in the current group Contrary to WindowTabs this is an interactive widget. The window that currently has focus is highlighted. """ orientations = base.ORIENTATION_HORIZONTAL defaults = [ ("font", "Arial", "Default font"), ("fontsize", None, "Font size. Calculated if None."), ("foreground", "ffffff", "Foreground colour"), ( "fontshadow", None, "font shadow color, default is None(no shadow)" ), ("borderwidth", 2, "Current group border width"), ("border", "215578", "Border colour"), ("rounded", True, "To round or not to round borders"), ( "highlight_method", "border", "Method of highlighting (one of 'border' or 'block') " "Uses \*_border color settings" ), ("urgent_border", "FF0000", "Urgent border color"), ( "urgent_alert_method", "border", "Method for alerting you of WM urgent " "hints (one of 'border' or 'text')" ), ("max_title_width", 200, "size in pixels of task title") ] def __init__(self, **config): base._Widget.__init__(self, bar.STRETCH, **config) self.add_defaults(TaskList.defaults) self.add_defaults(base.PaddingMixin.defaults) self.add_defaults(base.MarginMixin.defaults) self._icons_cache = {} def box_width(self, text): width, _ = self.drawer.max_layout_size( [text], self.font, self.fontsize ) width = width + self.padding_x * 2 + \ self.margin_x * 2 + self.borderwidth * 2 if width > self.max_title_width: width = self.max_title_width return width def _configure(self, qtile, bar): base._Widget._configure(self, qtile, bar) self.icon_size = self.bar.height - (self.borderwidth + 2) * 2 if self.fontsize is None: calc = self.bar.height - self.margin_y * 2 - \ self.borderwidth * 2 - self.padding_y * 2 self.fontsize = max(calc, 1) self.layout = self.drawer.textlayout( "", "ffffff", self.font, self.fontsize, self.fontshadow, wrap=False ) self.setup_hooks() def update(self, window=None): group = self.bar.screen.group if not window or window and window.group is group: self.bar.draw() def remove_icon_cache(self, window): wid = window.window.wid if wid in self._icons_cache: self._icons_cache.pop(wid) def invalidate_cache(self, window): self.remove_icon_cache(window) self.update(window) def setup_hooks(self): hook.subscribe.window_name_change(self.update) hook.subscribe.focus_change(self.update) hook.subscribe.float_change(self.update) hook.subscribe.client_urgent_hint_changed(self.update) hook.subscribe.net_wm_icon_change(self.invalidate_cache) hook.subscribe.client_killed(self.remove_icon_cache) def drawtext(self, text, textcolor, width): self.layout.text = text self.layout.font_family = self.font self.layout.font_size = self.fontsize self.layout.colour = textcolor if width is not None: self.layout.width = width def drawbox(self, offset, text, bordercolor, textcolor, width=None, rounded=False, block=False, icon=None): self.drawtext(text, textcolor, width) icon_padding = (self.icon_size + 4) if icon else 0 padding_x = [self.padding_x + icon_padding, self.padding_x] framed = self.layout.framed( self.borderwidth, bordercolor, padding_x, self.padding_y ) if block: framed.draw_fill(offset, self.margin_y, rounded) else: framed.draw(offset, self.margin_y, rounded) if icon: self.draw_icon(icon, offset) def get_clicked(self, x, y): window = None new_width = width = 0 for w in self.bar.screen.group.windows: new_width += self.icon_size + self.box_width(w.name) if width <= x <= new_width: window = w break width = new_width return window def button_press(self, x, y, button): window = None current_win = self.bar.screen.group.currentWindow # TODO: support scroll if button == 1: window = self.get_clicked(x, y) if window and window is not current_win: window.group.focus(window, False) if window.floating: window.cmd_bring_to_front() elif window: window.toggle_minimize() def get_window_icon(self, window): if not window.icons: return None cache = self._icons_cache.get(window.window.wid) if cache: return cache icons = sorted( iter(window.icons.items()), key=lambda x: abs(self.icon_size - int(x[0].split("x")[0])) ) icon = icons[0] width, height = map(int, icon[0].split("x")) img = cairocffi.ImageSurface.create_for_data( icon[1], cairocffi.FORMAT_ARGB32, width, height ) surface = cairocffi.SurfacePattern(img) scaler = cairocffi.Matrix() if height != self.icon_size: sp = height / self.icon_size height = self.icon_size width /= sp scaler.scale(sp, sp) surface.set_matrix(scaler) self._icons_cache[window.window.wid] = surface return surface def draw_icon(self, surface, offset): if not surface: return x = offset + self.padding_x + self.borderwidth + 2 + self.margin_x y = self.padding_y + self.borderwidth self.drawer.ctx.save() self.drawer.ctx.translate(x, y) self.drawer.ctx.set_source(surface) self.drawer.ctx.paint() self.drawer.ctx.restore() def draw(self): self.drawer.clear(self.background or self.bar.background) offset = 0 for w in self.bar.screen.group.windows: state = '' if w is None: pass elif w.maximized: state = '[] ' elif w.minimized: state = '_ ' elif w.floating: state = 'V ' task = "%s%s" % (state, w.name if w and w.name else " ") if w.urgent: border = self.urgent_border text_color = border elif w is w.group.currentWindow: border = self.border text_color = border else: border = self.background or self.bar.background text_color = self.foreground if self.highlight_method == 'text': border = self.bar.background else: text_color = self.foreground bw = self.box_width(task) self.drawbox( self.margin_x + offset, task, border, text_color, rounded=self.rounded, block=(self.highlight_method == 'block'), width=(bw - self.margin_x * 2 - self.padding_x * 2), icon=self.get_window_icon(w), ) offset += bw + self.icon_size self.drawer.draw(offsetx=self.offset, width=self.width) qtile-0.10.7/libqtile/widget/textbox.py000066400000000000000000000043041305063162100200520ustar00rootroot00000000000000# Copyright (c) 2008, 2010 Aldo Cortesi # Copyright (c) 2011 Mounier Florian # Copyright (c) 2012, 2015 Tycho Andersen # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2013 Craig Barnes # Copyright (c) 2014 Sean Vig # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from .. import bar from . import base class TextBox(base._TextBox): """A flexible textbox that can be updated from bound keys, scripts, and qshell""" orientations = base.ORIENTATION_HORIZONTAL defaults = [ ("font", "Arial", "Text font"), ("fontsize", None, "Font pixel size. Calculated if None."), ("fontshadow", None, "font shadow color, default is None(no shadow)"), ("padding", None, "Padding left and right. Calculated if None."), ("foreground", "#ffffff", "Foreground colour."), ] def __init__(self, text=" ", width=bar.CALCULATED, **config): base._TextBox.__init__(self, text=text, width=width, **config) def update(self, text): self.text = text self.bar.draw() def cmd_update(self, text): """Update the text in a TextBox widget""" self.update(text) def cmd_get(self): """Retrieve the text in a TextBox widget""" return self.text qtile-0.10.7/libqtile/widget/volume.py000066400000000000000000000205641305063162100176720ustar00rootroot00000000000000# Copyright (c) 2010, 2012, 2014 roger # Copyright (c) 2011 Kirk Strauser # Copyright (c) 2011 Florian Mounier # Copyright (c) 2011 Mounier Florian # Copyright (c) 2011 Roger Duran # Copyright (c) 2012-2015 Tycho Andersen # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2013 Craig Barnes # Copyright (c) 2014-2015 Sean Vig # Copyright (c) 2014 Adi Sieker # Copyright (c) 2014 dmpayton # Copyright (c) 2014 Jody Frankowski # Copyright (c) 2016 Christoph Lassner # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import os import re import subprocess import cairocffi from . import base from .. import bar from libqtile.log_utils import logger __all__ = [ 'Volume', ] re_vol = re.compile('\[(\d?\d?\d?)%\]') BUTTON_UP = 4 BUTTON_DOWN = 5 BUTTON_MUTE = 1 class Volume(base._TextBox): """Widget that display and change volume If theme_path is set it draw widget as icons. """ orientations = base.ORIENTATION_HORIZONTAL defaults = [ ("cardid", None, "Card Id"), ("device", "default", "Device Name"), ("channel", "Master", "Channel"), ("padding", 3, "Padding left and right. Calculated if None."), ("theme_path", None, "Path of the icons"), ("update_interval", 0.2, "Update time in seconds."), ("emoji", False, "Use emoji to display volume states, only if ``theme_path`` is not set." "The specified font needs to contain the correct unicode characters."), ("mute_command", None, "Mute command"), ("volume_up_command", None, "Volume up command"), ("volume_down_command", None, "Volume down command"), ("get_volume_command", None, "Command to get the current volume"), ] def __init__(self, **config): base._TextBox.__init__(self, '0', width=bar.CALCULATED, **config) self.add_defaults(Volume.defaults) if self.theme_path: self.length_type = bar.STATIC self.length = 0 self.surfaces = {} self.volume = None def timer_setup(self): self.timeout_add(self.update_interval, self.update) if self.theme_path: self.setup_images() def create_amixer_command(self, *args): cmd = ['amixer'] if (self.cardid is not None): cmd.extend(['-c', str(self.cardid)]) if (self.device is not None): cmd.extend(['-D', str(self.device)]) cmd.extend([x for x in args]) return cmd def button_press(self, x, y, button): if button == BUTTON_DOWN: if self.volume_down_command is not None: subprocess.call(self.volume_down_command) else: subprocess.call(self.create_amixer_command('-q', 'sset', self.channel, '2%-')) elif button == BUTTON_UP: if self.volume_up_command is not None: subprocess.call(self.volume_up_command) else: subprocess.call(self.create_amixer_command('-q', 'sset', self.channel, '2%+')) elif button == BUTTON_MUTE: if self.mute_command is not None: subprocess.call(self.mute_command) else: subprocess.call(self.create_amixer_command('-q', 'sset', self.channel, 'toggle')) self.draw() def update(self): vol = self.get_volume() if vol != self.volume: self.volume = vol # Update the underlying canvas size before actually attempting # to figure out how big it is and draw it. self._update_drawer() self.bar.draw() self.timeout_add(self.update_interval, self.update) def _update_drawer(self): if self.theme_path: self.drawer.clear(self.background or self.bar.background) if self.volume <= 0: img_name = 'audio-volume-muted' elif self.volume <= 30: img_name = 'audio-volume-low' elif self.volume < 80: img_name = 'audio-volume-medium' else: # self.volume >= 80: img_name = 'audio-volume-high' self.drawer.ctx.set_source(self.surfaces[img_name]) self.drawer.ctx.paint() elif self.emoji: if self.volume <= 0: self.text = u'\U0001f507' elif self.volume <= 30: self.text = u'\U0001f508' elif self.volume < 80: self.text = u'\U0001f509' elif self.volume >= 80: self.text = u'\U0001f50a' else: if self.volume == -1: self.text = 'M' else: self.text = '%s%%' % self.volume def setup_images(self): for img_name in ( 'audio-volume-high', 'audio-volume-low', 'audio-volume-medium', 'audio-volume-muted' ): try: img = cairocffi.ImageSurface.create_from_png( os.path.join(self.theme_path, '%s.png' % img_name) ) except cairocffi.Error: self.theme_path = None self.length_type = bar.CALCULATED logger.exception('Volume switching to text mode') return input_width = img.get_width() input_height = img.get_height() sp = input_height / float(self.bar.height - 1) width = input_width / sp if width > self.length: self.length = int(width) + self.actual_padding * 2 imgpat = cairocffi.SurfacePattern(img) scaler = cairocffi.Matrix() scaler.scale(sp, sp) scaler.translate(self.actual_padding * -1, 0) imgpat.set_matrix(scaler) imgpat.set_filter(cairocffi.FILTER_BEST) self.surfaces[img_name] = imgpat def get_volume(self): try: get_volume_cmd = self.create_amixer_command('sget', self.channel) if self.get_volume_command: get_volume_cmd = self.get_volume_command mixer_out = self.call_process(get_volume_cmd) except subprocess.CalledProcessError: return -1 if '[off]' in mixer_out: return -1 volgroups = re_vol.search(mixer_out) if volgroups: return int(volgroups.groups()[0]) else: # this shouldn't happen return -1 def draw(self): if self.theme_path: self.drawer.draw(offsetx=self.offset, width=self.length) else: base._TextBox.draw(self) def cmd_increase_vol(self): # Emulate button press. self.button_press(0, 0, BUTTON_UP) def cmd_decrease_vol(self): # Emulate button press. self.button_press(0, 0, BUTTON_DOWN) def cmd_mute(self): # Emulate button press. self.button_press(0, 0, BUTTON_MUTE) qtile-0.10.7/libqtile/widget/wallpaper.py000066400000000000000000000065641305063162100203560ustar00rootroot00000000000000# Copyright (c) 2015 Muhammed Abuali # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # # To use this widget, you will need to install feh wallpaper changer import os import subprocess import random from . import base from .. import bar from libqtile.log_utils import logger class Wallpaper(base._TextBox): defaults = [ ("directory", "~/Pictures/wallpapers/", "Wallpaper Directory"), ("wallpaper", None, "Wallpaper"), ("wallpaper_command", None, "Wallpaper command"), ("random_selection", False, "If set, use random initial wallpaper and " "randomly cycle through the wallpapers."), ("label", None, "Use a fixed label instead of image name.") ] def __init__(self, **config): base._TextBox.__init__(self, 'empty', width=bar.CALCULATED, **config) self.add_defaults(Wallpaper.defaults) self.index = 0 self.images = [] self.get_wallpapers() self.set_wallpaper() def get_path(self, file): return os.path.join(os.path.expanduser(self.directory), file) def get_wallpapers(self): try: # get path of all files in the directory self.images = list( filter(os.path.isfile, map(self.get_path, os.listdir(self.directory)))) except IOError as e: logger.exception("I/O error(%s): %s", e.errno, e.strerror) def set_wallpaper(self): if len(self.images) == 0: if self.wallpaper is None: self.text = "empty" return else: self.images.append(self.wallpaper) cur_image = self.images[self.index] if self.label is None: self.text = os.path.basename(cur_image) else: self.text = self.label if self.wallpaper_command: self.wallpaper_command.append(cur_image) subprocess.call(self.wallpaper_command) return subprocess.call([ 'feh', '--bg-fill', cur_image ]) def button_press(self, x, y, button): if button == 1: if self.random_selection: self.index = random.randint(0, len(self.images) - 1) else: self.index += 1 self.index %= len(self.images) self.set_wallpaper() self.draw() qtile-0.10.7/libqtile/widget/windowname.py000066400000000000000000000053001305063162100205220ustar00rootroot00000000000000# Copyright (c) 2008, 2010 Aldo Cortesi # Copyright (c) 2010 matt # Copyright (c) 2011 Mounier Florian # Copyright (c) 2012 Tim Neumann # Copyright (c) 2013 Craig Barnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Tycho Andersen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from .. import hook, bar from . import base class WindowName(base._TextBox): """Displays the name of the window that currently has focus""" orientations = base.ORIENTATION_HORIZONTAL defaults = [ ('show_state', True, 'show window status before window name'), ('for_current_screen', False, 'instead of this bars screen use currently active screen') ] def __init__(self, width=bar.STRETCH, **config): base._TextBox.__init__(self, width=width, **config) self.add_defaults(WindowName.defaults) def _configure(self, qtile, bar): base._TextBox._configure(self, qtile, bar) hook.subscribe.window_name_change(self.update) hook.subscribe.focus_change(self.update) hook.subscribe.float_change(self.update) @hook.subscribe.current_screen_change def on_screen_changed(): if self.for_current_screen: self.update() def update(self): if self.for_current_screen: w = self.qtile.currentScreen.group.currentWindow else: w = self.bar.screen.group.currentWindow state = '' if self.show_state and w is not None: if w.maximized: state = '[] ' elif w.minimized: state = '_ ' elif w.floating: state = 'V ' self.text = "%s%s" % (state, w.name if w and w.name else " ") self.bar.draw() qtile-0.10.7/libqtile/widget/windowtabs.py000066400000000000000000000055321305063162100205420ustar00rootroot00000000000000# Copyright (c) 2012-2013 Craig Barnes # Copyright (c) 2012 roger # Copyright (c) 2012, 2014 Tycho Andersen # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Adi Sieker # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from .. import hook, bar from . import base class WindowTabs(base._TextBox): """ Displays the name of each window in the current group. Contrary to TaskList this is not an interactive widget. The window that currently has focus is highlighted. """ orientations = base.ORIENTATION_HORIZONTAL defaults = [ ("separator", " | ", "Task separator text."), ("selected", ("<", ">"), "Selected task indicator"), ] def __init__(self, **config): base._TextBox.__init__(self, width=bar.STRETCH, **config) self.add_defaults(WindowTabs.defaults) if not isinstance(self.selected, (tuple, list)): self.selected = (self.selected, self.selected) def _configure(self, qtile, bar): base._TextBox._configure(self, qtile, bar) hook.subscribe.window_name_change(self.update) hook.subscribe.focus_change(self.update) hook.subscribe.float_change(self.update) def button_press(self, x, y, button): self.bar.screen.group.cmd_next_window() def update(self): names = [] for w in self.bar.screen.group.windows: state = '' if w is None: pass elif w.maximized: state = '[] ' elif w.minimized: state = '_ ' elif w.floating: state = 'V ' task = "%s%s" % (state, w.name if w and w.name else " ") if w is self.bar.screen.group.currentWindow: task = task.join(self.selected) names.append(task) self.text = self.separator.join(names) self.bar.draw() qtile-0.10.7/libqtile/widget/wlan.py000066400000000000000000000065151305063162100173240ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2012 Sebastian Bechtel # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 Sebastian Kricner # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Tycho Andersen # Copyright (c) 2014 Craig Barnes # Copyright (c) 2015 farebord # Copyright (c) 2015 Jörg Thalheim (Mic92) # Copyright (c) 2016 Juhani Imberg # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from . import base from libqtile.log_utils import logger try: from pythonwifi.iwlibs import Wireless, Iwstats def get_status(interface): interface = Wireless(interface) try: stats = Iwstats(interface) except IOError: return (None, None) quality = stats.qual.quality essid = interface.getEssid() return (essid, quality) except ImportError: import iwlib def get_status(interface): interface = iwlib.get_iwconfig(interface) if 'stats' not in interface: return (None, None) quality = interface['stats']['quality'] essid = bytes(interface['ESSID']).decode() return (essid, quality) class Wlan(base.InLoopPollText): """Displays Wifi ssid and quality""" orientations = base.ORIENTATION_HORIZONTAL defaults = [ ('interface', 'wlan0', 'The interface to monitor'), ('update_interval', 1, 'The update interval.'), ( 'disconnected_message', 'Disconnected', 'String to show when the wlan is diconnected.' ), ( 'format', '{essid} {quality}/70', 'Display format. For percents you can use "{essid} {percent:2.0%}"' ) ] def __init__(self, **config): base.InLoopPollText.__init__(self, **config) self.add_defaults(Wlan.defaults) def poll(self): try: essid, quality = get_status(self.interface) disconnected = essid is None if disconnected: return self.disconnected_message return self.format.format( essid=essid, quality=quality, percent=(quality / 70) ) except EnvironmentError: logger.error( '%s: Probably your wlan device is switched off or ' ' otherwise not present in your system.', self.__class__.__name__) qtile-0.10.7/libqtile/widget/yahoo_weather.py000066400000000000000000000116631305063162100212210ustar00rootroot00000000000000# -*- coding:utf-8 -*- # Copyright (c) 2011-2012 dmpayton # Copyright (c) 2011 Kenji_Takahashi # Copyright (c) 2011 Mounier Florian # Copyright (c) 2012, 2014-2015 Tycho Andersen # Copyright (c) 2013 David R. Andersen # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 Sean Vig # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from . import base from .generic_poll_text import GenPollUrl from xml.dom import minidom from six.moves.urllib.parse import urlencode QUERY_URL = 'http://query.yahooapis.com/v1/public/yql?' WEATHER_URL = 'http://weather.yahooapis.com/forecastrss?' WEATHER_NS = 'http://xml.weather.yahoo.com/ns/rss/1.0' class YahooWeather(GenPollUrl): """A weather widget, data provided by the Yahoo! Weather API. Format options: - astronomy_sunrise - astronomy_sunset - atmosphere_humidity - atmosphere_visibility - atmosphere_pressure - atmosphere_rising - condition_text - condition_code - condition_temp - condition_date - location_city - location_region - location_country - units_temperature - units_distance - units_pressure - units_speed - wind_chill """ orientations = base.ORIENTATION_HORIZONTAL defaults = [ # One of (location, woeid) must be set. ( 'location', None, 'Location to fetch weather for. Ignored if woeid is set.' ), ( 'woeid', None, 'Where On Earth ID. Auto-calculated if location is set.' ), ( 'format', '{location_city}: {condition_temp} °{units_temperature}', 'Display format' ), ('metric', True, 'True to use metric/C, False to use imperial/F'), ('up', '^', 'symbol for rising atmospheric pressure'), ('down', 'v', 'symbol for falling atmospheric pressure'), ('steady', 's', 'symbol for steady atmospheric pressure'), ] json = False def __init__(self, **config): GenPollUrl.__init__(self, **config) self.add_defaults(YahooWeather.defaults) self._url = None def fetch_woeid(self, location): url = QUERY_URL + urlencode({ 'q': 'select woeid from geo.places where text="%s"' % location, 'format': 'json' }) data = self.fetch(url) if data['query']['count'] > 1: return data['query']['results']['place'][0]['woeid'] return data['query']['results']['place']['woeid'] @property def url(self): if self._url: return self._url if not self.woeid: if self.location: self.woeid = self.fetch_woeid(self.location) if not self.woeid: return None format = 'c' if self.metric else 'f' self._url = WEATHER_URL + urlencode({'w': self.woeid, 'u': format}) return self._url def parse(self, body): dom = minidom.parseString(body) structure = ( ('location', ('city', 'region', 'country')), ('units', ('temperature', 'distance', 'pressure', 'speed')), ('wind', ('chill', 'direction', 'speed')), ('atmosphere', ('humidity', 'visibility', 'pressure', 'rising')), ('astronomy', ('sunrise', 'sunset')), ('condition', ('text', 'code', 'temp', 'date')) ) data = {} for tag, attrs in structure: element = dom.getElementsByTagNameNS(WEATHER_NS, tag)[0] for attr in attrs: data['%s_%s' % (tag, attr)] = element.getAttribute(attr) if data['atmosphere_rising'] == '0': data['atmosphere_rising'] = self.steady elif data['atmosphere_rising'] == '1': data['atmosphere_rising'] = self.up elif data['atmosphere_rising'] == '2': data['atmosphere_rising'] = self.down return self.format.format(**data) qtile-0.10.7/libqtile/window.py000066400000000000000000001303501305063162100164020ustar00rootroot00000000000000# Copyright (c) 2008, Aldo Cortesi. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import division import array import contextlib import inspect import traceback import warnings from xcffib.xproto import EventMask, StackMode, SetMode import xcffib.xproto from . import command from . import utils from . import hook from .log_utils import logger # ICCM Constants NoValue = 0x0000 XValue = 0x0001 YValue = 0x0002 WidthValue = 0x0004 HeightValue = 0x0008 AllValues = 0x000F XNegative = 0x0010 YNegative = 0x0020 USPosition = (1 << 0) USSize = (1 << 1) PPosition = (1 << 2) PSize = (1 << 3) PMinSize = (1 << 4) PMaxSize = (1 << 5) PResizeInc = (1 << 6) PAspect = (1 << 7) PBaseSize = (1 << 8) PWinGravity = (1 << 9) PAllHints = (PPosition | PSize | PMinSize | PMaxSize | PResizeInc | PAspect) InputHint = (1 << 0) StateHint = (1 << 1) IconPixmapHint = (1 << 2) IconWindowHint = (1 << 3) IconPositionHint = (1 << 4) IconMaskHint = (1 << 5) WindowGroupHint = (1 << 6) MessageHint = (1 << 7) UrgencyHint = (1 << 8) AllHints = (InputHint | StateHint | IconPixmapHint | IconWindowHint | IconPositionHint | IconMaskHint | WindowGroupHint | MessageHint | UrgencyHint) WithdrawnState = 0 DontCareState = 0 NormalState = 1 ZoomState = 2 IconicState = 3 InactiveState = 4 RectangleOut = 0 RectangleIn = 1 RectanglePart = 2 VisualNoMask = 0x0 VisualIDMask = 0x1 VisualScreenMask = 0x2 VisualDepthMask = 0x4 VisualClassMask = 0x8 VisualRedMaskMask = 0x10 VisualGreenMaskMask = 0x20 VisualBlueMaskMask = 0x40 VisualColormapSizeMask = 0x80 VisualBitsPerRGBMask = 0x100 VisualAllMask = 0x1FF ReleaseByFreeingColormap = 1 BitmapSuccess = 0 BitmapOpenFailed = 1 BitmapFileInvalid = 2 BitmapNoMemory = 3 XCSUCCESS = 0 XCNOMEM = 1 XCNOENT = 2 # float states NOT_FLOATING = 1 # not floating FLOATING = 2 MAXIMIZED = 3 FULLSCREEN = 4 TOP = 5 MINIMIZED = 6 _NET_WM_STATE_REMOVE = 0 _NET_WM_STATE_ADD = 1 _NET_WM_STATE_TOGGLE = 2 def _geometry_getter(attr): def get_attr(self): if getattr(self, "_" + attr) is None: g = self.window.get_geometry() # trigger the geometry setter on all these self.x = g.x self.y = g.y self.width = g.width self.height = g.height return getattr(self, "_" + attr) return get_attr def _geometry_setter(attr): def f(self, value): if not isinstance(value, int): frame = inspect.currentframe() stack_trace = traceback.format_stack(frame) logger.error("!!!! setting %s to a non-int %s; please report this!", attr, value) logger.error(''.join(stack_trace[:-1])) value = int(value) setattr(self, "_" + attr, value) return f def _float_getter(attr): def getter(self): if self._float_info[attr] is not None: return self._float_info[attr] # we don't care so much about width or height, if not set, default to the window width/height if attr in ('width', 'height'): return getattr(self, attr) raise AttributeError("Floating not yet configured yet") return getter def _float_setter(attr): def setter(self, value): self._float_info[attr] = value return setter class _Window(command.CommandObject): _windowMask = None # override in child class def __init__(self, window, qtile): self.window, self.qtile = window, qtile self.hidden = True self.group = None self.icons = {} window.set_attribute(eventmask=self._windowMask) self._float_info = { 'x': None, 'y': None, 'width': None, 'height': None, } try: g = self.window.get_geometry() self._x = g.x self._y = g.y self._width = g.width self._height = g.height self._float_info['width'] = g.width self._float_info['height'] = g.height except xcffib.xproto.DrawableError: # Whoops, we were too early, so let's ignore it for now and get the # values on demand. self._x = None self._y = None self._width = None self._height = None self.borderwidth = 0 self.bordercolor = None self.name = "" self.strut = None self.state = NormalState self.window_type = "normal" self._float_state = NOT_FLOATING self._demands_attention = False self.hints = { 'input': True, 'icon_pixmap': None, 'icon_window': None, 'icon_x': 0, 'icon_y': 0, 'icon_mask': 0, 'window_group': None, 'urgent': False, # normal or size hints 'width_inc': None, 'height_inc': None, 'base_width': 0, 'base_height': 0, } self.updateHints() x = property(fset=_geometry_setter("x"), fget=_geometry_getter("x")) y = property(fset=_geometry_setter("y"), fget=_geometry_getter("y")) width = property( fset=_geometry_setter("width"), fget=_geometry_getter("width") ) height = property( fset=_geometry_setter("height"), fget=_geometry_getter("height") ) float_x = property( fset=_float_setter("x"), fget=_float_getter("x") ) float_y = property( fset=_float_setter("y"), fget=_float_getter("y") ) float_width = property( fset=_float_setter("width"), fget=_float_getter("width") ) float_height = property( fset=_float_setter("height"), fget=_float_getter("height") ) @property def has_focus(self): return self == self.qtile.currentWindow def updateName(self): try: self.name = self.window.get_name() except (xcffib.xproto.WindowError, xcffib.xproto.AccessError): return hook.fire("window_name_change") def updateHints(self): """Update the local copy of the window's WM_HINTS See http://tronche.com/gui/x/icccm/sec-4.html#WM_HINTS """ try: h = self.window.get_wm_hints() normh = self.window.get_wm_normal_hints() except (xcffib.xproto.WindowError, xcffib.xproto.AccessError): return # FIXME # h values # { # 'icon_pixmap': 4194337, # 'icon_window': 0, # 'icon_mask': 4194340, # 'icon_y': 0, # 'input': 1, # 'icon_x': 0, # 'window_group': 4194305 # 'initial_state': 1, # 'flags': set(['StateHint', # 'IconMaskHint', # 'WindowGroupHint', # 'InputHint', # 'UrgencyHint', # 'IconPixmapHint']), # } if normh: normh.pop('flags') normh['min_width'] = max(0, normh.get('min_width', 0)) normh['min_height'] = max(0, normh.get('min_height', 0)) if not normh['base_width'] and \ normh['min_width'] and \ normh['width_inc']: # seems xcffib does ignore base width :( normh['base_width'] = ( normh['min_width'] % normh['width_inc'] ) if not normh['base_height'] and \ normh['min_height'] and \ normh['height_inc']: # seems xcffib does ignore base height :( normh['base_height'] = ( normh['min_height'] % normh['height_inc'] ) self.hints.update(normh) if h and 'UrgencyHint' in h['flags']: if self.qtile.currentWindow != self: self.hints['urgent'] = True hook.fire('client_urgent_hint_changed', self) elif self.urgent: self.hints['urgent'] = False hook.fire('client_urgent_hint_changed', self) if getattr(self, 'group', None): self.group.layoutAll() return def updateState(self): triggered = ['urgent'] if self.qtile.config.auto_fullscreen: triggered.append('fullscreen') state = self.window.get_net_wm_state() logger.debug('_NET_WM_STATE: %s', state) for s in triggered: setattr(self, s, (s in state)) @property def urgent(self): return self.hints['urgent'] or self._demands_attention @urgent.setter def urgent(self, val): self._demands_attention = val # TODO unset window hint as well? if not val: self.hints['urgent'] = False def info(self): if self.group: group = self.group.name else: group = None return dict( name=self.name, x=self.x, y=self.y, width=self.width, height=self.height, group=group, id=self.window.wid, floating=self._float_state != NOT_FLOATING, float_info=self._float_info, maximized=self._float_state == MAXIMIZED, minimized=self._float_state == MINIMIZED, fullscreen=self._float_state == FULLSCREEN ) @property def state(self): return self.window.get_wm_state()[0] @state.setter def state(self, val): if val in (WithdrawnState, NormalState, IconicState): self.window.set_property('WM_STATE', [val, 0]) def setOpacity(self, opacity): if 0.0 <= opacity <= 1.0: real_opacity = int(opacity * 0xffffffff) self.window.set_property('_NET_WM_WINDOW_OPACITY', real_opacity) else: return def getOpacity(self): opacity = self.window.get_property( "_NET_WM_WINDOW_OPACITY", unpack=int ) if not opacity: return 1.0 else: value = opacity[0] # 2 decimal places as_float = round(value / 0xffffffff, 2) return as_float opacity = property(getOpacity, setOpacity) def kill(self): if "WM_DELETE_WINDOW" in self.window.get_wm_protocols(): data = [ self.qtile.conn.atoms["WM_DELETE_WINDOW"], xcffib.xproto.Time.CurrentTime, 0, 0, 0 ] u = xcffib.xproto.ClientMessageData.synthetic(data, "I" * 5) e = xcffib.xproto.ClientMessageEvent.synthetic( format=32, window=self.window.wid, type=self.qtile.conn.atoms["WM_PROTOCOLS"], data=u ) self.window.send_event(e) else: self.window.kill_client() def hide(self): # We don't want to get the UnmapNotify for this unmap with self.disableMask(xcffib.xproto.EventMask.StructureNotify): self.window.unmap() self.hidden = True def unhide(self): self.window.map() self.state = NormalState self.hidden = False @contextlib.contextmanager def disableMask(self, mask): self._disableMask(mask) yield self._resetMask() def _disableMask(self, mask): self.window.set_attribute( eventmask=self._windowMask & (~mask) ) def _resetMask(self): self.window.set_attribute( eventmask=self._windowMask ) def place(self, x, y, width, height, borderwidth, bordercolor, above=False, force=False, margin=None): """Places the window at the specified location with the given size. If force is false, than it tries to obey hints """ # TODO: self.x/y/height/width are updated BEFORE # place is called, so there's no way to know if only # the position is changed, so we are sending # the ConfigureNotify every time place is called # # # if position change and size don't # # send a configure notify. See ICCCM 4.2.3 # send_notify = False # if (self.x != x or self.y != y) and \ # (self.width == width and self.height == height): # send_notify = True # #for now, we just: send_notify = True # Adjust the placement to account for layout margins, if there are any. if margin is not None: x += margin y += margin width -= margin * 2 height -= margin * 2 # save x and y float offset if self.group is not None and self.group.screen is not None: self.float_x = x - self.group.screen.x self.float_y = y - self.group.screen.y self.x = x self.y = y self.width = width self.height = height self.borderwidth = borderwidth self.bordercolor = bordercolor kwarg = dict( x=x, y=y, width=width, height=height, borderwidth=borderwidth, ) if above: kwarg['stackmode'] = StackMode.Above self.window.configure(**kwarg) if send_notify: self.send_configure_notify(x, y, width, height) if bordercolor is not None: self.window.set_attribute(borderpixel=bordercolor) def send_configure_notify(self, x, y, width, height): """Send a synthetic ConfigureNotify""" window = self.window.wid above_sibling = False override_redirect = False event = xcffib.xproto.ConfigureNotifyEvent.synthetic( event=window, window=window, above_sibling=above_sibling, x=x, y=y, width=width, height=height, border_width=self.borderwidth, override_redirect=override_redirect ) self.window.send_event(event, mask=EventMask.StructureNotify) def can_steal_focus(self): return self.window.get_wm_type() != 'notification' def focus(self, warp): # Workaround for misbehaving java applications (actually it might be # qtile who misbehaves by not implementing some X11 protocol correctly) # # See this xmonad issue for more information on the problem: # http://code.google.com/p/xmonad/issues/detail?id=177 # # 'sun-awt-X11-XFramePeer' is a main window of a java application. # Only send WM_TAKE_FOCUS not FocusIn # 'sun-awt-X11-XDialogPeer' is a dialog of a java application. Do not # send any event. cls = self.window.get_wm_class() or '' is_java_main = 'sun-awt-X11-XFramePeer' in cls is_java_dialog = 'sun-awt-X11-XDialogPeer' in cls is_java = is_java_main or is_java_dialog if not self.hidden: # Never send TAKE_FOCUS on java *dialogs* if not is_java_dialog and \ "WM_TAKE_FOCUS" in self.window.get_wm_protocols(): data = [ self.qtile.conn.atoms["WM_TAKE_FOCUS"], xcffib.xproto.Time.CurrentTime, 0, 0, 0 ] u = xcffib.xproto.ClientMessageData.synthetic(data, "I" * 5) e = xcffib.xproto.ClientMessageEvent.synthetic( format=32, window=self.window.wid, type=self.qtile.conn.atoms["WM_PROTOCOLS"], data=u ) self.window.send_event(e) # Never send FocusIn to java windows if not is_java and self.hints['input']: self.window.set_input_focus() try: if warp and self.qtile.config.cursor_warp: self.window.warp_pointer(self.width // 2, self.height // 2) except AttributeError: pass if self.urgent: self.urgent = False atom = self.qtile.conn.atoms["_NET_WM_STATE_DEMANDS_ATTENTION"] state = list(self.window.get_property('_NET_WM_STATE', 'ATOM', unpack=int)) if atom in state: state.remove(atom) self.window.set_property('_NET_WM_STATE', state) self.qtile.root.set_property("_NET_ACTIVE_WINDOW", self.window.wid) hook.fire("client_focus", self) def _items(self, name, sel): return None def _select(self, name, sel): return None def cmd_focus(self, warp=None): """Focuses the window.""" if warp is None: warp = self.qtile.config.cursor_warp self.focus(warp=warp) def cmd_info(self): """Returns a dictionary of info for this object""" return self.info() def cmd_inspect(self): """Tells you more than you ever wanted to know about a window""" a = self.window.get_attributes() attrs = { "backing_store": a.backing_store, "visual": a.visual, "class": a._class, "bit_gravity": a.bit_gravity, "win_gravity": a.win_gravity, "backing_planes": a.backing_planes, "backing_pixel": a.backing_pixel, "save_under": a.save_under, "map_is_installed": a.map_is_installed, "map_state": a.map_state, "override_redirect": a.override_redirect, # "colormap": a.colormap, "all_event_masks": a.all_event_masks, "your_event_mask": a.your_event_mask, "do_not_propagate_mask": a.do_not_propagate_mask } props = self.window.list_properties() normalhints = self.window.get_wm_normal_hints() hints = self.window.get_wm_hints() protocols = [] for i in self.window.get_wm_protocols(): protocols.append(i) state = self.window.get_wm_state() return dict( attributes=attrs, properties=props, name=self.window.get_name(), wm_class=self.window.get_wm_class(), wm_window_role=self.window.get_wm_window_role(), wm_type=self.window.get_wm_type(), wm_transient_for=self.window.get_wm_transient_for(), protocols=protocols, wm_icon_name=self.window.get_wm_icon_name(), wm_client_machine=self.window.get_wm_client_machine(), normalhints=normalhints, hints=hints, state=state, float_info=self._float_info ) class Internal(_Window): """An internal window, that should not be managed by qtile""" _windowMask = EventMask.StructureNotify | \ EventMask.PropertyChange | \ EventMask.EnterWindow | \ EventMask.FocusChange | \ EventMask.Exposure | \ EventMask.ButtonPress | \ EventMask.ButtonRelease | \ EventMask.KeyPress @classmethod def create(cls, qtile, x, y, width, height, opacity=1.0): win = qtile.conn.create_window(x, y, width, height) win.set_property("QTILE_INTERNAL", 1) i = Internal(win, qtile) i.place(x, y, width, height, 0, None) i.opacity = opacity return i def __repr__(self): return "Internal(%r, %s)" % (self.name, self.window.wid) def kill(self): self.qtile.conn.conn.core.DestroyWindow(self.window.wid) def cmd_kill(self): self.kill() class Static(_Window): """An internal window, that should not be managed by qtile""" _windowMask = EventMask.StructureNotify | \ EventMask.PropertyChange | \ EventMask.EnterWindow | \ EventMask.FocusChange | \ EventMask.Exposure def __init__(self, win, qtile, screen, x=None, y=None, width=None, height=None): _Window.__init__(self, win, qtile) self.updateName() self.conf_x = x self.conf_y = y self.conf_width = width self.conf_height = height self.x = x or 0 self.y = y or 0 self.width = width or 0 self.height = height or 0 self.screen = screen if None not in (x, y, width, height): self.place(x, y, width, height, 0, 0) self.update_strut() def handle_ConfigureRequest(self, e): cw = xcffib.xproto.ConfigWindow if self.conf_x is None and e.value_mask & cw.X: self.x = e.x if self.conf_y is None and e.value_mask & cw.Y: self.y = e.y if self.conf_width is None and e.value_mask & cw.Width: self.width = e.width if self.conf_height is None and e.value_mask & cw.Height: self.height = e.height self.place( self.screen.x + self.x, self.screen.y + self.y, self.width, self.height, self.borderwidth, self.bordercolor ) return False def update_strut(self): strut = self.window.get_property( "_NET_WM_STRUT_PARTIAL", unpack=int ) strut = strut or self.window.get_property( "_NET_WM_STRUT", unpack=int ) strut = strut or (0, 0, 0, 0) self.qtile.update_gaps(strut, self.strut) self.strut = strut def handle_PropertyNotify(self, e): name = self.qtile.conn.atoms.get_name(e.atom) if name in ("_NET_WM_STRUT_PARTIAL", "_NET_WM_STRUT"): self.update_strut() def __repr__(self): return "Static(%r)" % self.name class Window(_Window): _windowMask = EventMask.StructureNotify | \ EventMask.PropertyChange | \ EventMask.EnterWindow | \ EventMask.FocusChange # Set when this object is being retired. defunct = False def __init__(self, window, qtile): _Window.__init__(self, window, qtile) self._group = None self.updateName() # add to group by position according to _NET_WM_DESKTOP property group = None index = window.get_wm_desktop() if index is not None and index < len(qtile.groups): group = qtile.groups[index] elif index is None: transient_for = window.get_wm_transient_for() win = qtile.windowMap.get(transient_for) if win is not None: group = win._group if group is not None: group.add(self) self._group = group if group != qtile.currentScreen.group: self.hide() # add window to the save-set, so it gets mapped when qtile dies qtile.conn.conn.core.ChangeSaveSet(SetMode.Insert, self.window.wid) self.update_wm_net_icon() @property def group(self): return self._group @group.setter def group(self, group): if group: try: self.window.set_property( "_NET_WM_DESKTOP", self.qtile.groups.index(group) ) except xcffib.xproto.WindowError: logger.exception("whoops, got error setting _NET_WM_DESKTOP, too early?") self._group = group @property def edges(self): return (self.x, self.y, self.x + self.width, self.y + self.height) @property def floating(self): return self._float_state != NOT_FLOATING @floating.setter def floating(self, do_float): if do_float and self._float_state == NOT_FLOATING: if self.group and self.group.screen: screen = self.group.screen self._enablefloating( screen.x + self.float_x, screen.y + self.float_y, self.float_width, self.float_height ) else: # if we are setting floating early, e.g. from a hook, we don't have a screen yet self._float_state = FLOATING elif (not do_float) and self._float_state != NOT_FLOATING: if self._float_state == FLOATING: # store last size self.float_width = self.width self.float_height = self.height self._float_state = NOT_FLOATING self.group.mark_floating(self, False) hook.fire('float_change') def toggle_floating(self): self.floating = not self.floating def togglefloating(self): warnings.warn("togglefloating is deprecated, use toggle_floating", DeprecationWarning) self.toggle_floating() def enablefloating(self): warnings.warn("enablefloating is deprecated, use floating=True", DeprecationWarning) self.floating = True def disablefloating(self): warnings.warn("disablefloating is deprecated, use floating=False", DeprecationWarning) self.floating = False @property def fullscreen(self): return self._float_state == FULLSCREEN @fullscreen.setter def fullscreen(self, do_full): atom = set([self.qtile.conn.atoms["_NET_WM_STATE_FULLSCREEN"]]) prev_state = set(self.window.get_property('_NET_WM_STATE', 'ATOM', unpack=int)) if do_full: screen = self.group.screen or \ self.qtile.find_closest_screen(self.x, self.y) self._enablefloating( screen.x, screen.y, screen.width, screen.height, new_float_state=FULLSCREEN ) state = prev_state | atom else: if self._float_state == FULLSCREEN: self.floating = False state = prev_state - atom else: state = prev_state if prev_state != state: self.window.set_property('_NET_WM_STATE', list(state)) def toggle_fullscreen(self): self.fullscreen = not self.fullscreen def togglefullscreen(self): warnings.warn("togglefullscreen is deprecated, use toggle_fullscreen", DeprecationWarning) self.toggle_fullscreen() @property def maximized(self): return self._float_state == MAXIMIZED @maximized.setter def maximized(self, do_maximize): if do_maximize: screen = self.group.screen or \ self.qtile.find_closest_screen(self.x, self.y) self._enablefloating( screen.dx, screen.dy, screen.dwidth, screen.dheight, new_float_state=MAXIMIZED ) else: if self._float_state == MAXIMIZED: self.floating = False def enablemaximize(self, state=MAXIMIZED): warnings.warn("enablemaximize is deprecated, use maximized=True", DeprecationWarning) self.maximized = True def toggle_maximize(self, state=MAXIMIZED): self.maximized = not self.maximized def togglemaximize(self): warnings.warn("togglemaximize is deprecated, use toggle_maximize", DeprecationWarning) self.toggle_maximize() @property def minimized(self): return self._float_state == MINIMIZED @minimized.setter def minimized(self, do_minimize): if do_minimize: if self._float_state != MINIMIZED: self._enablefloating(new_float_state=MINIMIZED) else: if self._float_state == MINIMIZED: self.floating = False def enableminimize(self): warnings.warn("enableminimized is deprecated, use minimized=True", DeprecationWarning) self.minimized = True def toggle_minimize(self): self.minimized = not self.minimized def toggleminimize(self): warnings.warn("toggleminimize is deprecated, use toggle_minimize", DeprecationWarning) self.toggle_minimize() def static(self, screen, x=None, y=None, width=None, height=None): """Makes this window a static window, attached to a Screen If any of the arguments are left unspecified, the values given by the window itself are used instead. So, for a window that's aware of its appropriate size and location (like dzen), you don't have to specify anything. """ self.defunct = True screen = self.qtile.screens[screen] if self.group: self.group.remove(self) s = Static(self.window, self.qtile, screen, x, y, width, height) self.qtile.windowMap[self.window.wid] = s hook.fire("client_managed", s) return s def tweak_float(self, x=None, y=None, dx=0, dy=0, w=None, h=None, dw=0, dh=0): if x is not None: self.x = x self.x += dx if y is not None: self.y = y self.y += dy if w is not None: self.width = w self.width += dw if h is not None: self.height = h self.height += dh if self.height < 0: self.height = 0 if self.width < 0: self.width = 0 screen = self.qtile.find_closest_screen(self.x, self.y) if self.group and screen is not None and screen != self.group.screen: self.group.remove(self, force=True) screen.group.add(self, force=True) self.qtile.toScreen(screen.index) self._reconfigure_floating() def getsize(self): return (self.width, self.height) def getposition(self): return (self.x, self.y) def _reconfigure_floating(self, new_float_state=FLOATING): if new_float_state == MINIMIZED: self.state = IconicState self.hide() else: width = max(self.width, self.hints.get('min_width', 0)) height = max(self.height, self.hints.get('min_height', 0)) if self.hints['base_width'] and self.hints['width_inc']: width -= (width - self.hints['base_width']) % self.hints['width_inc'] if self.hints['base_height'] and self.hints['height_inc']: height -= (height - self.hints['base_height']) % self.hints['height_inc'] self.place( self.x, self.y, width, height, self.borderwidth, self.bordercolor, above=True, ) if self._float_state != new_float_state: self._float_state = new_float_state if self.group: # may be not, if it's called from hook self.group.mark_floating(self, True) hook.fire('float_change') def _enablefloating(self, x=None, y=None, w=None, h=None, new_float_state=FLOATING): if new_float_state != MINIMIZED: self.x = x self.y = y self.width = w self.height = h self._reconfigure_floating(new_float_state=new_float_state) def togroup(self, groupName=None): """Move window to a specified group""" if groupName is None: group = self.qtile.currentGroup else: group = self.qtile.groupMap.get(groupName) if group is None: raise command.CommandError("No such group: %s" % groupName) if self.group is not group: self.hide() if self.group: if self.group.screen: # for floats remove window offset self.x -= self.group.screen.x self.group.remove(self) if group.screen and self.x < group.screen.x: self.x += group.screen.x group.add(self) def toscreen(self, index=None): """ Move window to a specified screen, or the current screen. """ if index is None: screen = self.qtile.currentScreen else: try: screen = self.qtile.screens[index] except IndexError: raise command.CommandError('No such screen: %d' % index) self.togroup(screen.group.name) def match(self, wname=None, wmclass=None, role=None): """Match window against given attributes. Parameters ========== wname : matches against the window name or title, that is, either ``_NET_WM_VISIBLE_NAME``, ``_NET_WM_NAME``, ``WM_NAME``. wmclass : matches against any of the two values in the ``WM_CLASS`` property role : matches against the ``WM_WINDOW_ROLE`` property """ if not (wname or wmclass or role): raise TypeError( "Either a name, a wmclass or a role must be specified" ) if wname and wname == self.name: return True try: cliclass = self.window.get_wm_class() if wmclass and cliclass and wmclass in cliclass: return True clirole = self.window.get_wm_window_role() if role and clirole and role == clirole: return True except (xcffib.xproto.WindowError, xcffib.xproto.AccessError): return False return False def handle_EnterNotify(self, e): hook.fire("client_mouse_enter", self) if self.qtile.config.follow_mouse_focus and \ self.group.currentWindow != self: self.group.focus(self, False) if self.group.screen and \ self.qtile.currentScreen != self.group.screen and \ self.qtile.config.follow_mouse_focus: self.qtile.toScreen(self.group.screen.index, False) return True def handle_ConfigureRequest(self, e): if self.qtile._drag and self.qtile.currentWindow == self: # ignore requests while user is dragging window return if getattr(self, 'floating', False): # only obey resize for floating windows cw = xcffib.xproto.ConfigWindow width = e.width if e.value_mask & cw.Width else self.width height = e.height if e.value_mask & cw.Height else self.height x = e.x if e.value_mask & cw.X else self.x y = e.y if e.value_mask & cw.Y else self.y else: width, height, x, y = self.width, self.height, self.x, self.y if self.group and self.group.screen: self.place( x, y, width, height, self.borderwidth, self.bordercolor, ) self.updateState() return False def update_wm_net_icon(self): """Set a dict with the icons of the window""" icon = self.window.get_property('_NET_WM_ICON', 'CARDINAL') if not icon: return icon = list(map(ord, icon.value)) icons = {} while True: if not icon: break size = icon[:8] if len(size) != 8 or not size[0] or not size[4]: break icon = icon[8:] width = size[0] height = size[4] next_pix = width * height * 4 data = icon[:next_pix] arr = array.array("B", data) for i in range(0, len(arr), 4): mult = arr[i + 3] / 255. arr[i + 0] = int(arr[i + 0] * mult) arr[i + 1] = int(arr[i + 1] * mult) arr[i + 2] = int(arr[i + 2] * mult) icon = icon[next_pix:] icons["%sx%s" % (width, height)] = arr self.icons = icons hook.fire("net_wm_icon_change", self) def handle_ClientMessage(self, event): atoms = self.qtile.conn.atoms opcode = event.type data = event.data if atoms["_NET_WM_STATE"] == opcode: prev_state = self.window.get_property( '_NET_WM_STATE', 'ATOM', unpack=int ) current_state = set(prev_state) action = data.data32[0] for prop in (data.data32[1], data.data32[2]): if not prop: # skip 0 continue if action == _NET_WM_STATE_REMOVE: current_state.discard(prop) elif action == _NET_WM_STATE_ADD: current_state.add(prop) elif action == _NET_WM_STATE_TOGGLE: current_state ^= set([prop]) # toggle :D self.window.set_property('_NET_WM_STATE', list(current_state)) elif atoms["_NET_ACTIVE_WINDOW"] == opcode: source = data.data32[0] if source == 2: # XCB_EWMH_CLIENT_SOURCE_TYPE_NORMAL logger.info("Focusing window by pager") self.qtile.currentScreen.setGroup(self.group) self.group.focus(self) else: # XCB_EWMH_CLIENT_SOURCE_TYPE_OTHER focus_behavior = self.qtile.config.focus_on_window_activation if focus_behavior == "focus" or (focus_behavior == "smart" and self.group.screen and self.group.screen == self.qtile.currentScreen): logger.info("Focusing window") self.qtile.currentScreen.setGroup(self.group) self.group.focus(self) elif focus_behavior == "urgent" or (focus_behavior == "smart" and not self.group.screen): logger.info("Setting urgent flag for window") self.urgent = True else: logger.info("Ignoring focus request") def handle_PropertyNotify(self, e): name = self.qtile.conn.atoms.get_name(e.atom) logger.debug("PropertyNotifyEvent: %s", name) if name == "WM_TRANSIENT_FOR": pass elif name == "WM_HINTS": self.updateHints() elif name == "WM_NORMAL_HINTS": self.updateHints() elif name == "WM_NAME": self.updateName() elif name == "_NET_WM_NAME": self.updateName() elif name == "_NET_WM_VISIBLE_NAME": self.updateName() elif name == "WM_ICON_NAME": pass elif name == "_NET_WM_ICON_NAME": pass elif name == "_NET_WM_ICON": self.update_wm_net_icon() elif name == "ZOOM": pass elif name == "_NET_WM_WINDOW_OPACITY": pass elif name == "WM_STATE": pass elif name == "_NET_WM_STATE": self.updateState() elif name == "WM_PROTOCOLS": pass elif name == "_NET_WM_DESKTOP": # Some windows set the state(fullscreen) when starts, # updateState is here because the group and the screen # are set when the property is emitted # self.updateState() self.updateState() elif name == "_NET_WM_USER_TIME": if not self.qtile.config.follow_mouse_focus and \ self.group.currentWindow != self: self.group.focus(self, False) else: logger.info("Unknown window property: %s", name) return False def _items(self, name): if name == "group": return (True, None) elif name == "layout": return (True, list(range(len(self.group.layouts)))) elif name == "screen": return (True, None) def _select(self, name, sel): if name == "group": return self.group elif name == "layout": if sel is None: return self.group.layout else: return utils.lget(self.group.layouts, sel) elif name == "screen": return self.group.screen def __repr__(self): return "Window(%r)" % self.name def cmd_static(self, screen, x, y, width, height): self.static(screen, x, y, width, height) def cmd_kill(self): """Kill this window Try to do this politely if the client support this, otherwise be brutal. """ self.kill() def cmd_togroup(self, groupName=None): """Move window to a specified group. If groupName is not specified, we assume the current group Examples ======== Move window to current group:: togroup() Move window to group "a":: togroup("a") """ self.togroup(groupName) def cmd_toscreen(self, index=None): """Move window to a specified screen. If index is not specified, we assume the current screen Examples ======== Move window to current screen:: toscreen() Move window to screen 0:: toscreen(0) """ self.toscreen(index) def cmd_move_floating(self, dx, dy, curx, cury): """Move window by dx and dy""" self.tweak_float(dx=dx, dy=dy) def cmd_resize_floating(self, dw, dh, curx, cury): """Add dw and dh to size of window""" self.tweak_float(dw=dw, dh=dh) def cmd_set_position_floating(self, x, y, curx, cury): """Move window to x and y""" self.tweak_float(x=x, y=y) def cmd_set_size_floating(self, w, h, curx, cury): """Set window dimensions to w and h""" self.tweak_float(w=w, h=h) def cmd_get_position(self): return self.getposition() def cmd_get_size(self): return self.getsize() def cmd_toggle_floating(self): self.toggle_floating() def cmd_enable_floating(self): self.floating = True def cmd_disable_floating(self): self.floating = False def cmd_toggle_maximize(self): self.toggle_maximize() def cmd_enable_maximize(self): self.maximize = True def cmd_disable_maximize(self): self.maximize = False def cmd_toggle_fullscreen(self): self.toggle_fullscreen() def cmd_enable_fullscreen(self): self.fullscreen = True def cmd_disable_fullscreen(self): self.fullscreen = False def cmd_toggle_minimize(self): self.toggle_minimize() def cmd_enable_minimize(self): self.minimize = True def cmd_disable_minimize(self): self.minimize = False def cmd_bring_to_front(self): if self.floating: self.window.configure(stackmode=StackMode.Above) else: self._reconfigure_floating() # atomatically above def cmd_match(self, *args, **kwargs): return self.match(*args, **kwargs) def cmd_opacity(self, opacity): if opacity < .1: self.opacity = .1 elif opacity > 1: self.opacity = 1 else: self.opacity = opacity def cmd_down_opacity(self): if self.opacity > .2: # don't go completely clear self.opacity -= .1 else: self.opacity = .1 def cmd_up_opacity(self): if self.opacity < .9: self.opacity += .1 else: self.opacity = 1 def _is_in_window(self, x, y, window): return (window.edges[0] <= x <= window.edges[2] and window.edges[1] <= y <= window.edges[3]) def cmd_set_position(self, dx, dy, curx, cury): if self.floating: self.tweak_float(dx, dy) return for window in self.group.windows: if window == self or window.floating: continue if self._is_in_window(curx, cury, window): clients = self.group.layout.clients index1 = clients.index(self) index2 = clients.index(window) clients[index1], clients[index2] = clients[index2], clients[index1] self.group.layout.focused = index2 self.group.layoutAll() break qtile-0.10.7/libqtile/xcbq.py000066400000000000000000000741131305063162100160340ustar00rootroot00000000000000# Copyright (c) 2009-2010 Aldo Cortesi # Copyright (c) 2010 matt # Copyright (c) 2010, 2012, 2014 dequis # Copyright (c) 2010 Philip Kranz # Copyright (c) 2010-2011 Paul Colomiets # Copyright (c) 2011 osebelin # Copyright (c) 2011 Mounier Florian # Copyright (c) 2011 Kenji_Takahashi # Copyright (c) 2011 Tzbob # Copyright (c) 2012, 2014 roger # Copyright (c) 2012, 2014-2015 Tycho Andersen # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014-2015 Sean Vig # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """ A minimal EWMH-aware OO layer over xpyb. This is NOT intended to be complete - it only implements the subset of functionalty needed by qtile. """ from __future__ import print_function, division import six from xcffib.xproto import CW, WindowClass, EventMask from xcffib.xfixes import SelectionEventMask import xcffib import xcffib.randr import xcffib.xinerama import xcffib.xproto from . import xkeysyms from .log_utils import logger from .xcursors import Cursors keysyms = xkeysyms.keysyms def rdict(d): r = {} for k, v in d.items(): r.setdefault(v, []).append(k) return r rkeysyms = rdict(xkeysyms.keysyms) # These should be in xpyb: ModMasks = { "shift": 1 << 0, "lock": 1 << 1, "control": 1 << 2, "mod1": 1 << 3, "mod2": 1 << 4, "mod3": 1 << 5, "mod4": 1 << 6, "mod5": 1 << 7, } ModMapOrder = [ "shift", "lock", "control", "mod1", "mod2", "mod3", "mod4", "mod5" ] AllButtonsMask = 0b11111 << 8 ButtonMotionMask = 1 << 13 ButtonReleaseMask = 1 << 3 NormalHintsFlags = { "USPosition": 1, # User-specified x, y "USSize": 2, # User-specified width, height "PPosition": 4, # Program-specified position "PSize": 8, # Program-specified size "PMinSize": 16, # Program-specified minimum size "PMaxSize": 32, # Program-specified maximum size "PResizeInc": 64, # Program-specified resize increments "PAspect": 128, # Program-specified min and max aspect ratios "PBaseSize": 256, # Program-specified base size "PWinGravity": 512, # Program-specified window gravity } HintsFlags = { "InputHint": 1, # input "StateHint": 2, # initial_state "IconPixmapHint": 4, # icon_pixmap "IconWindowHint": 8, # icon_window "IconPositionHint": 16, # icon_x & icon_y "IconMaskHint": 32, # icon_mask "WindowGroupHint": 64, # window_group "MessageHint": 128, # (this bit is obsolete) "UrgencyHint": 256, # urgency } # http://standards.freedesktop.org/wm-spec/latest/ar01s05.html#idm139870830002400 WindowTypes = { '_NET_WM_WINDOW_TYPE_DESKTOP': "desktop", '_NET_WM_WINDOW_TYPE_DOCK': "dock", '_NET_WM_WINDOW_TYPE_TOOLBAR': "toolbar", '_NET_WM_WINDOW_TYPE_MENU': "menu", '_NET_WM_WINDOW_TYPE_UTILITY': "utility", '_NET_WM_WINDOW_TYPE_SPLASH': "splash", '_NET_WM_WINDOW_TYPE_DIALOG': "dialog", '_NET_WM_WINDOW_TYPE_DROPDOWN_MENU': "dropdown", '_NET_WM_WINDOW_TYPE_POPUP_MENU': "menu", '_NET_WM_WINDOW_TYPE_TOOLTIP': "tooltip", '_NET_WM_WINDOW_TYPE_NOTIFICATION': "notification", '_NET_WM_WINDOW_TYPE_COMBO': "combo", '_NET_WM_WINDOW_TYPE_DND': "dnd", '_NET_WM_WINDOW_TYPE_NORMAL': "normal", } # http://standards.freedesktop.org/wm-spec/latest/ar01s05.html#idm139870829988448 WindowStates = { None: 'normal', '_NET_WM_STATE_FULLSCREEN': 'fullscreen', '_NET_WM_STATE_DEMANDS_ATTENTION': 'urgent' } # Maps property names to types and formats. PropertyMap = { # ewmh properties "_NET_DESKTOP_GEOMETRY": ("CARDINAL", 32), "_NET_SUPPORTED": ("ATOM", 32), "_NET_SUPPORTING_WM_CHECK": ("WINDOW", 32), "_NET_WM_NAME": ("UTF8_STRING", 8), "_NET_WM_PID": ("CARDINAL", 32), "_NET_CLIENT_LIST": ("WINDOW", 32), "_NET_CLIENT_LIST_STACKING": ("WINDOW", 32), "_NET_NUMBER_OF_DESKTOPS": ("CARDINAL", 32), "_NET_CURRENT_DESKTOP": ("CARDINAL", 32), "_NET_DESKTOP_NAMES": ("UTF8_STRING", 8), "_NET_WORKAREA": ("CARDINAL", 32), "_NET_ACTIVE_WINDOW": ("WINDOW", 32), "_NET_WM_DESKTOP": ("CARDINAL", 32), "_NET_WM_STRUT": ("CARDINAL", 32), "_NET_WM_STRUT_PARTIAL": ("CARDINAL", 32), "_NET_WM_WINDOW_OPACITY": ("CARDINAL", 32), "_NET_WM_WINDOW_TYPE": ("CARDINAL", 32), # Net State "_NET_WM_STATE": ("ATOM", 32), "_NET_WM_STATE_STICKY": ("ATOM", 32), "_NET_WM_STATE_SKIP_TASKBAR": ("ATOM", 32), "_NET_WM_STATE_FULLSCREEN": ("ATOM", 32), "_NET_WM_STATE_MAXIMIZED_HORZ": ("ATOM", 32), "_NET_WM_STATE_MAXIMIZED_VERT": ("ATOM", 32), "_NET_WM_STATE_ABOVE": ("ATOM", 32), "_NET_WM_STATE_BELOW": ("ATOM", 32), "_NET_WM_STATE_MODAL": ("ATOM", 32), "_NET_WM_STATE_HIDDEN": ("ATOM", 32), "_NET_WM_STATE_DEMANDS_ATTENTION": ("ATOM", 32), # Xembed "_XEMBED_INFO": ("_XEMBED_INFO", 32), # ICCCM "WM_STATE": ("WM_STATE", 32), # Qtile-specific properties "QTILE_INTERNAL": ("CARDINAL", 32) } # TODO add everything required here: # http://standards.freedesktop.org/wm-spec/latest/ar01s03.html SUPPORTED_ATOMS = [ # From http://standards.freedesktop.org/wm-spec/latest/ar01s03.html '_NET_SUPPORTED', '_NET_CLIENT_LIST', '_NET_CLIENT_LIST_STACKING', '_NET_CURRENT_DESKTOP', '_NET_ACTIVE_WINDOW', # '_NET_WORKAREA', '_NET_SUPPORTING_WM_CHECK', # From http://standards.freedesktop.org/wm-spec/latest/ar01s05.html '_NET_WM_NAME', '_NET_WM_VISIBLE_NAME', '_NET_WM_ICON_NAME', '_NET_WM_DESKTOP', '_NET_WM_WINDOW_TYPE', '_NET_WM_STATE', '_NET_WM_STRUT', '_NET_WM_STRUT_PARTIAL', '_NET_WM_PID', ] SUPPORTED_ATOMS.extend(WindowTypes.keys()) SUPPORTED_ATOMS.extend(key for key in WindowStates.keys() if key) XCB_CONN_ERRORS = { 1: 'XCB_CONN_ERROR', 2: 'XCB_CONN_CLOSED_EXT_NOTSUPPORTED', 3: 'XCB_CONN_CLOSED_MEM_INSUFFICIENT', 4: 'XCB_CONN_CLOSED_REQ_LEN_EXCEED', 5: 'XCB_CONN_CLOSED_PARSE_ERR', 6: 'XCB_CONN_CLOSED_INVALID_SCREEN', 7: 'XCB_CONN_CLOSED_FDPASSING_FAILED', } class MaskMap(object): """ A general utility class that encapsulates the way the mask/value idiom works in xpyb. It understands a special attribute _maskvalue on objects, which will be used instead of the object value if present. This lets us pass in a Font object, rather than Font.fid, for example. """ def __init__(self, obj): self.mmap = [] for i in dir(obj): if not i.startswith("_"): self.mmap.append((getattr(obj, i), i.lower())) self.mmap.sort() def __call__(self, **kwargs): """ kwargs: keys should be in the mmap name set Returns a (mask, values) tuple. """ mask = 0 values = [] for m, s in self.mmap: if s in kwargs: val = kwargs.get(s) if val is not None: mask |= m values.append(getattr(val, "_maskvalue", val)) del kwargs[s] if kwargs: raise ValueError("Unknown mask names: %s" % list(kwargs.keys())) return mask, values ConfigureMasks = MaskMap(xcffib.xproto.ConfigWindow) AttributeMasks = MaskMap(CW) GCMasks = MaskMap(xcffib.xproto.GC) class AtomCache(object): def __init__(self, conn): self.conn = conn self.atoms = {} self.reverse = {} # We can change the pre-loads not to wait for a return for name in WindowTypes.keys(): self.insert(name=name) for i in dir(xcffib.xproto.Atom): if not i.startswith("_"): self.insert(name=i, atom=getattr(xcffib.xproto.Atom, i)) def insert(self, name=None, atom=None): assert name or atom if atom is None: c = self.conn.conn.core.InternAtom(False, len(name), name) atom = c.reply().atom if name is None: c = self.conn.conn.core.GetAtomName(atom) name = c.reply().name.to_string() self.atoms[name] = atom self.reverse[atom] = name def get_name(self, atom): if atom not in self.reverse: self.insert(atom=atom) return self.reverse[atom] def __getitem__(self, key): if key not in self.atoms: self.insert(name=key) return self.atoms[key] class _Wrapper(object): def __init__(self, wrapped): self.wrapped = wrapped def __getattr__(self, x): return getattr(self.wrapped, x) class Screen(_Wrapper): """ This represents an actual X screen. """ def __init__(self, conn, screen): _Wrapper.__init__(self, screen) self.default_colormap = Colormap(conn, screen.default_colormap) self.root = Window(conn, self.root) class PseudoScreen(object): """ This may be a Xinerama screen or a RandR CRTC, both of which are rectangular sections of an actual Screen. """ def __init__(self, conn, x, y, width, height): self.conn = conn self.x = x self.y = y self.width = width self.height = height class Colormap(object): def __init__(self, conn, cid): self.conn = conn self.cid = cid def alloc_color(self, color): """ Flexible color allocation. """ try: return self.conn.conn.core.AllocNamedColor( self.cid, len(color), color ).reply() except xcffib.xproto.NameError: def x8to16(i): return 0xffff * (i & 0xff) // 0xff r = x8to16(int(color[-6] + color[-5], 16)) g = x8to16(int(color[-4] + color[-3], 16)) b = x8to16(int(color[-2] + color[-1], 16)) return self.conn.conn.core.AllocColor(self.cid, r, g, b).reply() class Xinerama(object): def __init__(self, conn): self.ext = conn.conn(xcffib.xinerama.key) def query_screens(self): r = self.ext.QueryScreens().reply() return r.screen_info class RandR(object): def __init__(self, conn): self.ext = conn.conn(xcffib.randr.key) self.ext.SelectInput( conn.default_screen.root.wid, xcffib.randr.NotifyMask.ScreenChange ) def query_crtcs(self, root): l = [] for i in self.ext.GetScreenResources(root).reply().crtcs: info = self.ext.GetCrtcInfo(i, xcffib.CurrentTime).reply() d = dict( x=info.x, y=info.y, width=info.width, height=info.height ) l.append(d) return l class XFixes(object): selection_mask = SelectionEventMask.SetSelectionOwner | \ SelectionEventMask.SelectionClientClose | \ SelectionEventMask.SelectionWindowDestroy def __init__(self, conn): self.conn = conn self.ext = conn.conn(xcffib.xfixes.key) self.ext.QueryVersion(xcffib.xfixes.MAJOR_VERSION, xcffib.xfixes.MINOR_VERSION) def select_selection_input(self, window, selection="PRIMARY"): SELECTION = self.conn.atoms[selection] self.conn.xfixes.ext.SelectSelectionInput(window.wid, SELECTION, self.selection_mask) class GC(object): def __init__(self, conn, gid): self.conn = conn self.gid = gid def change(self, **kwargs): mask, values = GCMasks(**kwargs) self.conn.conn.core.ChangeGC(self.gid, mask, values) class Window(object): def __init__(self, conn, wid): self.conn = conn self.wid = wid def _propertyString(self, r): """Extract a string from a window property reply message""" return r.value.to_string() def _propertyUTF8(self, r): return r.value.to_utf8() def send_event(self, synthevent, mask=EventMask.NoEvent): self.conn.conn.core.SendEvent(False, self.wid, mask, synthevent.pack()) def kill_client(self): self.conn.conn.core.KillClient(self.wid) def set_input_focus(self): self.conn.conn.core.SetInputFocus( xcffib.xproto.InputFocus.PointerRoot, self.wid, xcffib.xproto.Time.CurrentTime ) def warp_pointer(self, x, y): """Warps the pointer to the location `x`, `y` on the window""" self.conn.conn.core.WarpPointer( 0, self.wid, # src_window, dst_window 0, 0, # src_x, src_y 0, 0, # src_width, src_height x, y # dest_x, dest_y ) def get_name(self): """Tries to retrieve a canonical window name. We test the following properties in order of preference: - _NET_WM_VISIBLE_NAME - _NET_WM_NAME - WM_NAME. """ r = self.get_property("_NET_WM_VISIBLE_NAME", "UTF8_STRING") if r: return self._propertyUTF8(r) r = self.get_property("_NET_WM_NAME", "UTF8_STRING") if r: return self._propertyUTF8(r) r = self.get_property(xcffib.xproto.Atom.WM_NAME, "UTF8_STRING") if r: return self._propertyUTF8(r) r = self.get_property( xcffib.xproto.Atom.WM_NAME, xcffib.xproto.GetPropertyType.Any ) if r: return self._propertyString(r) def get_wm_hints(self): r = self.get_property("WM_HINTS", xcffib.xproto.GetPropertyType.Any) if r: l = r.value.to_atoms() flags = set(k for k, v in HintsFlags.items() if l[0] & v) return dict( flags=flags, input=l[1] if "InputHint" in flags else None, initial_state=l[2] if "StateHing" in flags else None, icon_pixmap=l[3] if "IconPixmapHint" in flags else None, icon_window=l[4] if "IconWindowHint" in flags else None, icon_x=l[5] if "IconPositionHint" in flags else None, icon_y=l[6] if "IconPositionHint" in flags else None, icon_mask=l[7] if "IconMaskHint" in flags else None, window_group=l[8] if 'WindowGroupHint' in flags else None, ) def get_wm_normal_hints(self): r = self.get_property( "WM_NORMAL_HINTS", xcffib.xproto.GetPropertyType.Any ) if r: l = r.value.to_atoms() flags = set(k for k, v in NormalHintsFlags.items() if l[0] & v) return dict( flags=flags, min_width=l[1 + 4], min_height=l[2 + 4], max_width=l[3 + 4], max_height=l[4 + 4], width_inc=l[5 + 4], height_inc=l[6 + 4], min_aspect=l[7 + 4], max_aspect=l[8 + 4], base_width=l[9 + 4], base_height=l[9 + 4], win_gravity=l[9 + 4], ) def get_wm_protocols(self): l = self.get_property("WM_PROTOCOLS", "ATOM", unpack=int) if l is not None: return set(self.conn.atoms.get_name(i) for i in l) return set() def get_wm_state(self): return self.get_property("WM_STATE", xcffib.xproto.GetPropertyType.Any, unpack=int) def get_wm_class(self): """Return an (instance, class) tuple if WM_CLASS exists, or None""" r = self.get_property("WM_CLASS", "STRING") if r: s = self._propertyString(r) return tuple(s.strip("\0").split("\0")) return tuple() def get_wm_window_role(self): r = self.get_property("WM_WINDOW_ROLE", "STRING") if r: return self._propertyString(r) def get_wm_transient_for(self): r = self.get_property("WM_TRANSIENT_FOR", "WINDOW", unpack=int) if r: return r[0] def get_wm_icon_name(self): r = self.get_property("_NET_WM_ICON_NAME", "UTF8_STRING") if r: return self._propertyUTF8(r) r = self.get_property("WM_ICON_NAME", "STRING") if r: return self._propertyUTF8(r) def get_wm_client_machine(self): r = self.get_property("WM_CLIENT_MACHINE", "STRING") if r: return self._propertyUTF8(r) def get_geometry(self): q = self.conn.conn.core.GetGeometry(self.wid) return q.reply() def get_wm_desktop(self): r = self.get_property("_NET_WM_DESKTOP", "CARDINAL", unpack=int) if r: return r[0] def get_wm_type(self): """ http://standards.freedesktop.org/wm-spec/wm-spec-latest.html#id2551529 """ r = self.get_property('_NET_WM_WINDOW_TYPE', "ATOM", unpack=int) if r: name = self.conn.atoms.get_name(r[0]) return WindowTypes.get(name, name) def get_net_wm_state(self): r = self.get_property('_NET_WM_STATE', "ATOM", unpack=int) if r: names = [self.conn.atoms.get_name(p) for p in r] return [WindowStates.get(n, n) for n in names] return [] def get_net_wm_pid(self): r = self.get_property("_NET_WM_PID", unpack=int) if r: return r[0] def configure(self, **kwargs): """ Arguments can be: x, y, width, height, border, sibling, stackmode """ mask, values = ConfigureMasks(**kwargs) # older versions of xcb pack everything into unsigned ints "=I" # since 1.12, uses switches to pack things sensibly if float(xcffib.__xcb_proto_version__) < 1.12: values = [i & 0xffffffff for i in values] return self.conn.conn.core.ConfigureWindow(self.wid, mask, values) def set_attribute(self, **kwargs): mask, values = AttributeMasks(**kwargs) self.conn.conn.core.ChangeWindowAttributesChecked( self.wid, mask, values ) def set_cursor(self, name): cursorId = self.conn.cursors[name] mask, values = AttributeMasks(cursor=cursorId) self.conn.conn.core.ChangeWindowAttributesChecked( self.wid, mask, values ) def set_property(self, name, value, type=None, format=None): """ Parameters ========== name : String Atom name type : String Atom name format : 8, 16, 32 """ if name in PropertyMap: if type or format: raise ValueError( "Over-riding default type or format for property." ) type, format = PropertyMap[name] else: if None in (type, format): raise ValueError( "Must specify type and format for unknown property." ) try: if isinstance(value, six.string_types): # xcffib will pack the bytes, but we should encode them properly if six.PY3: value = value.encode() elif not isinstance(value, str): # This will only run for Python 2 unicode strings, can't # use 'isinstance(value, unicode)' because Py 3 does not # have unicode and pyflakes complains value = value.encode('utf-8') else: # if this runs without error, the value is already a list, don't wrap it next(iter(value)) except StopIteration: # The value was an iterable, just empty value = [] except TypeError: # the value wasn't an iterable and wasn't a string, so let's # wrap it. value = [value] try: self.conn.conn.core.ChangePropertyChecked( xcffib.xproto.PropMode.Replace, self.wid, self.conn.atoms[name], self.conn.atoms[type], format, # Format - 8, 16, 32 len(value), value ).check() except xcffib.xproto.WindowError: logger.warning( 'X error in SetProperty (wid=%r, prop=%r), ignoring', self.wid, name) def get_property(self, prop, type=None, unpack=None): """Return the contents of a property as a GetPropertyReply If unpack is specified, a tuple of values is returned. The type to unpack, either `str` or `int` must be specified. """ if type is None: if prop not in PropertyMap: raise ValueError( "Must specify type for unknown property." ) else: type, _ = PropertyMap[prop] try: r = self.conn.conn.core.GetProperty( False, self.wid, self.conn.atoms[prop] if isinstance(prop, six.string_types) else prop, self.conn.atoms[type] if isinstance(type, six.string_types) else type, 0, (2 ** 32) - 1 ).reply() except (xcffib.xproto.WindowError, xcffib.xproto.AccessError): logger.warning( 'X error in GetProperty (wid=%r, prop=%r), ignoring', self.wid, prop) if unpack: return [] return None if not r.value_len: if unpack: return [] return None elif unpack: # Should we allow more options for unpacking? if unpack is int: return r.value.to_atoms() elif unpack is str: return r.value.to_string() else: return r def list_properties(self): r = self.conn.conn.core.ListProperties(self.wid).reply() return [self.conn.atoms.get_name(i) for i in r.atoms] def map(self): self.conn.conn.core.MapWindow(self.wid) def unmap(self): self.conn.conn.core.UnmapWindowChecked(self.wid).check() def get_attributes(self): return self.conn.conn.core.GetWindowAttributes(self.wid).reply() def create_gc(self, **kwargs): gid = self.conn.conn.generate_id() mask, values = GCMasks(**kwargs) self.conn.conn.core.CreateGC(gid, self.wid, mask, values) return GC(self.conn, gid) def ungrab_key(self, key, modifiers): """Passing None means any key, or any modifier""" if key is None: key = xcffib.xproto.Atom.Any if modifiers is None: modifiers = xcffib.xproto.ModMask.Any self.conn.conn.core.UngrabKey(key, self.wid, modifiers) def grab_key(self, key, modifiers, owner_events, pointer_mode, keyboard_mode): self.conn.conn.core.GrabKey( owner_events, self.wid, modifiers, key, pointer_mode, keyboard_mode ) def ungrab_button(self, button, modifiers): """Passing None means any key, or any modifier""" if button is None: button = xcffib.xproto.Atom.Any if modifiers is None: modifiers = xcffib.xproto.ModMask.Any self.conn.conn.core.UngrabButton(button, self.wid, modifiers) def grab_button(self, button, modifiers, owner_events, event_mask, pointer_mode, keyboard_mode): self.conn.conn.core.GrabButton( owner_events, self.wid, event_mask, pointer_mode, keyboard_mode, xcffib.xproto.Atom._None, xcffib.xproto.Atom._None, button, modifiers, ) def grab_pointer(self, owner_events, event_mask, pointer_mode, keyboard_mode, cursor=None): self.conn.conn.core.GrabPointer( owner_events, self.wid, event_mask, pointer_mode, keyboard_mode, xcffib.xproto.Atom._None, cursor or xcffib.xproto.Atom._None, xcffib.xproto.Atom._None, ) def ungrab_pointer(self): self.conn.conn.core.UngrabPointer(xcffib.xproto.Atom._None) def query_tree(self): q = self.conn.conn.core.QueryTree(self.wid).reply() root = None parent = None if q.root: root = Window(self.conn, q.root) if q.parent: parent = Window(self.conn, q.parent) return root, parent, [Window(self.conn, i) for i in q.children] class Font(object): def __init__(self, conn, fid): self.conn = conn self.fid = fid @property def _maskvalue(self): return self.fid def text_extents(self, s): s += "aaa" x = self.conn.conn.core.QueryTextExtents(self.fid, len(s), s).reply() return x class Connection(object): _extmap = { "xinerama": Xinerama, "randr": RandR, "xfixes": XFixes, } def __init__(self, display): self.conn = xcffib.connect(display=display) self._connected = True self.cursors = Cursors(self) self.setup = self.conn.get_setup() extensions = self.extensions() self.screens = [Screen(self, i) for i in self.setup.roots] self.default_screen = self.screens[self.conn.pref_screen] for i in extensions: if i in self._extmap: setattr(self, i, self._extmap[i](self)) self.pseudoscreens = [] if "xinerama" in extensions: for i, s in enumerate(self.xinerama.query_screens()): scr = PseudoScreen( self, s.x_org, s.y_org, s.width, s.height, ) self.pseudoscreens.append(scr) elif "randr" in extensions: for i in self.randr.query_crtcs(self.screens[0].root.wid): scr = PseudoScreen( self, i["x"], i["y"], i["width"], i["height"], ) self.pseudoscreens.append(scr) self.atoms = AtomCache(self) self.code_to_syms = {} self.first_sym_to_code = None self.refresh_keymap() self.modmap = None self.refresh_modmap() def finalize(self): self.cursors.finalize() self.disconnect() def refresh_keymap(self, first=None, count=None): if first is None: first = self.setup.min_keycode count = self.setup.max_keycode - self.setup.min_keycode + 1 q = self.conn.core.GetKeyboardMapping(first, count).reply() assert len(q.keysyms) % q.keysyms_per_keycode == 0 for i in range(len(q.keysyms) // q.keysyms_per_keycode): self.code_to_syms[first + i] = \ q.keysyms[i * q.keysyms_per_keycode:(i + 1) * q.keysyms_per_keycode] first_sym_to_code = {} for k, s in self.code_to_syms.items(): if s[0] and not s[0] in first_sym_to_code: first_sym_to_code[s[0]] = k self.first_sym_to_code = first_sym_to_code def refresh_modmap(self): q = self.conn.core.GetModifierMapping().reply() modmap = {} for i, k in enumerate(q.keycodes): l = modmap.setdefault(ModMapOrder[i // q.keycodes_per_modifier], []) l.append(k) self.modmap = modmap def get_modifier(self, keycode): """Return the modifier matching keycode""" for n, l in self.modmap.items(): if keycode in l: return n return None def keysym_to_keycode(self, keysym): return self.first_sym_to_code.get(keysym, 0) def keycode_to_keysym(self, keycode, modifier): if keycode >= len(self.code_to_syms) or \ modifier >= len(self.code_to_syms[keycode]): return 0 return self.code_to_syms[keycode][modifier] def create_window(self, x, y, width, height): wid = self.conn.generate_id() self.conn.core.CreateWindow( self.default_screen.root_depth, wid, self.default_screen.root.wid, x, y, width, height, 0, WindowClass.InputOutput, self.default_screen.root_visual, CW.BackPixel | CW.EventMask, [ self.default_screen.black_pixel, EventMask.StructureNotify | EventMask.Exposure ] ) return Window(self, wid) def disconnect(self): self.conn.disconnect() self._connected = False def flush(self): if self._connected: return self.conn.flush() def xsync(self): # The idea here is that pushing an innocuous request through the queue # and waiting for a response "syncs" the connection, since requests are # serviced in order. self.conn.core.GetInputFocus().reply() def grab_server(self): return self.conn.core.GrabServer() def get_setup(self): return self.conn.get_setup() def open_font(self, name): fid = self.conn.generate_id() self.conn.core.OpenFont(fid, len(name), name) return Font(self, fid) def extensions(self): return set( i.name.to_string().lower() for i in self.conn.core.ListExtensions().reply().names ) qtile-0.10.7/libqtile/xcursors.py000066400000000000000000000115321305063162100167630ustar00rootroot00000000000000from .log_utils import logger try: from ._ffi_xcursors import ffi except ImportError: # PyPy < 2.6 compaitibility import cffi if cffi.__version_info__[0] == 0: from .ffi_build import xcursors_ffi as ffi else: raise ImportError("No module named libqtile._ffi_xcursors, be sure to run `python ./libqtile/ffi_build.py`") # Stolen from samurai-x # (Don't know where to put it, so I'll put it here) # XCB cursors doesn't want to be themed, libxcursor # would be better choice I think # and we (indirectly) depend on it anyway... class Cursors(dict): def __init__(self, conn): self.conn = conn cursors = ( (b'X_cursor', 0), (b'arrow', 2), (b'based_arrow_down', 4), (b'based_arrow_up', 6), (b'boat', 8), (b'bogosity', 10), (b'bottom_left_corner', 12), (b'bottom_right_corner', 14), (b'bottom_side', 16), (b'bottom_tee', 18), (b'box_spiral', 20), (b'center_ptr', 22), (b'circle', 24), (b'clock', 26), (b'coffee_mug', 28), (b'cross', 30), (b'cross_reverse', 32), (b'crosshair', 34), (b'diamond_cross', 36), (b'dot', 38), (b'dotbox', 40), (b'double_arrow', 42), (b'draft_large', 44), (b'draft_small', 46), (b'draped_box', 48), (b'exchange', 50), (b'fleur', 52), (b'gobbler', 54), (b'gumby', 56), (b'hand1', 58), (b'hand2', 60), (b'heart', 62), (b'icon', 64), (b'iron_cross', 66), (b'left_ptr', 68), (b'left_side', 70), (b'left_tee', 72), (b'leftbutton', 74), (b'll_angle', 76), (b'lr_angle', 78), (b'man', 80), (b'middlebutton', 82), (b'mouse', 84), (b'pencil', 86), (b'pirate', 88), (b'plus', 90), (b'question_arrow', 92), (b'right_ptr', 94), (b'right_side', 96), (b'right_tee', 98), (b'rightbutton', 100), (b'rtl_logo', 102), (b'sailboat', 104), (b'sb_down_arrow', 106), (b'sb_h_double_arrow', 108), (b'sb_left_arrow', 110), (b'sb_right_arrow', 112), (b'sb_up_arrow', 114), (b'sb_v_double_arrow', 116), (b'shuttle', 118), (b'sizing', 120), (b'spider', 122), (b'spraycan', 124), (b'star', 126), (b'target', 128), (b'tcross', 130), (b'top_left_arrow', 132), (b'top_left_corner', 134), (b'top_right_corner', 136), (b'top_side', 138), (b'top_tee', 140), (b'trek', 142), (b'ul_angle', 144), (b'umbrella', 146), (b'ur_angle', 148), (b'watch', 150), (b'xterm', 152) ) self.xcursor = self._setup_xcursor_binding() for name, cursor_font in cursors: self._new(name, cursor_font) if self.xcursor: self.xcursor.xcb_cursor_context_free(self._cursor_ctx[0]) def finalize(self): self._cursor_ctx = None def _setup_xcursor_binding(self): try: xcursor = ffi.dlopen('libxcb-cursor.so') except OSError: logger.warning("xcb-cursor not found, fallback to font pointer") return False conn = self.conn.conn screen_pointer = conn.get_screen_pointers()[0] self._cursor_ctx = ffi.new('xcb_cursor_context_t **') xcursor.xcb_cursor_context_new(conn._conn, screen_pointer, self._cursor_ctx) return xcursor def get_xcursor(self, name): """ Get the cursor using xcb-util-cursor, so we support themed cursors """ cursor = self.xcursor.xcb_cursor_load_cursor(self._cursor_ctx[0], name) return cursor def get_font_cursor(self, name, cursor_font): """ Get the cursor from the font, used as a fallback if xcb-util-cursor is not installed """ fid = self.conn.conn.generate_id() self.conn.conn.core.OpenFont(fid, len("cursor"), "cursor") cursor = self.conn.conn.generate_id() self.conn.conn.core.CreateGlyphCursor( cursor, fid, fid, cursor_font, cursor_font + 1, 0, 0, 0, 65535, 65535, 65535 ) return cursor def _new(self, name, cursor_font): if self.xcursor: cursor = self.get_xcursor(name) else: cursor = self.get_font_cursor(name, cursor_font) self[name.decode()] = cursor qtile-0.10.7/libqtile/xkeysyms.py000066400000000000000000001740331305063162100167750ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2010 Aldo Cortesi # Copyright (c) 2012 Julian Berman # Copyright (c) 2014 Björn Lässig # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. keysyms = { 'XF86ModeLock': 0x1008FF01, 'XF86MonBrightnessUp': 0x1008FF02, 'XF86MonBrightnessDown': 0x1008FF03, 'XF86KbdLightOnOff': 0x1008FF04, 'XF86KbdBrightnessUp': 0x1008FF05, 'XF86KbdBrightnessDown': 0x1008FF06, 'XF86Standby': 0x1008FF10, 'XF86AudioLowerVolume': 0x1008FF11, 'XF86AudioMute': 0x1008FF12, 'XF86AudioRaiseVolume': 0x1008FF13, 'XF86AudioPlay': 0x1008FF14, 'XF86AudioStop': 0x1008FF15, 'XF86AudioPrev': 0x1008FF16, 'XF86AudioNext': 0x1008FF17, 'XF86HomePage': 0x1008FF18, 'XF86Mail': 0x1008FF19, 'XF86Start': 0x1008FF1A, 'XF86Search': 0x1008FF1B, 'XF86AudioRecord': 0x1008FF1C, 'XF86Calculator': 0x1008FF1D, 'XF86Memo': 0x1008FF1E, 'XF86ToDoList': 0x1008FF1F, 'XF86Calendar': 0x1008FF20, 'XF86PowerDown': 0x1008FF21, 'XF86ContrastAdjust': 0x1008FF22, 'XF86RockerUp': 0x1008FF23, 'XF86RockerDown': 0x1008FF24, 'XF86RockerEnter': 0x1008FF25, 'XF86Back': 0x1008FF26, 'XF86Forward': 0x1008FF27, 'XF86Stop': 0x1008FF28, 'XF86Refresh': 0x1008FF29, 'XF86PowerOff': 0x1008FF2A, 'XF86WakeUp': 0x1008FF2B, 'XF86Eject': 0x1008FF2C, 'XF86ScreenSaver': 0x1008FF2D, 'XF86WWW': 0x1008FF2E, 'XF86Sleep': 0x1008FF2F, 'XF86Favorites': 0x1008FF30, 'XF86AudioPause': 0x1008FF31, 'XF86AudioMedia': 0x1008FF32, 'XF86MyComputer': 0x1008FF33, 'XF86VendorHome': 0x1008FF34, 'XF86LightBulb': 0x1008FF35, 'XF86Shop': 0x1008FF36, 'XF86History': 0x1008FF37, 'XF86OpenURL': 0x1008FF38, 'XF86AddFavorite': 0x1008FF39, 'XF86HotLinks': 0x1008FF3A, 'XF86BrightnessAdjust': 0x1008FF3B, 'XF86Finance': 0x1008FF3C, 'XF86Community': 0x1008FF3D, 'XF86AudioRewind': 0x1008FF3E, 'XF86BackForward': 0x1008FF3F, 'XF86Launch0': 0x1008FF40, 'XF86Launch1': 0x1008FF41, 'XF86Launch2': 0x1008FF42, 'XF86Launch3': 0x1008FF43, 'XF86Launch4': 0x1008FF44, 'XF86Launch5': 0x1008FF45, 'XF86Launch6': 0x1008FF46, 'XF86Launch7': 0x1008FF47, 'XF86Launch8': 0x1008FF48, 'XF86Launch9': 0x1008FF49, 'XF86LaunchA': 0x1008FF4A, 'XF86LaunchB': 0x1008FF4B, 'XF86LaunchC': 0x1008FF4C, 'XF86LaunchD': 0x1008FF4D, 'XF86LaunchE': 0x1008FF4E, 'XF86LaunchF': 0x1008FF4F, 'XF86ApplicationLeft': 0x1008FF50, 'XF86ApplicationRight': 0x1008FF51, 'XF86Book': 0x1008FF52, 'XF86CD': 0x1008FF53, 'XF86Calculater': 0x1008FF54, 'XF86Clear': 0x1008FF55, 'XF86Close': 0x1008FF56, 'XF86Copy': 0x1008FF57, 'XF86Cut': 0x1008FF58, 'XF86Display': 0x1008FF59, 'XF86DOS': 0x1008FF5A, 'XF86Documents': 0x1008FF5B, 'XF86Excel': 0x1008FF5C, 'XF86Explorer': 0x1008FF5D, 'XF86Game': 0x1008FF5E, 'XF86Go': 0x1008FF5F, 'XF86iTouch': 0x1008FF60, 'XF86LogOff': 0x1008FF61, 'XF86Market': 0x1008FF62, 'XF86Meeting': 0x1008FF63, 'XF86MenuKB': 0x1008FF65, 'XF86MenuPB': 0x1008FF66, 'XF86MySites': 0x1008FF67, 'XF86New': 0x1008FF68, 'XF86News': 0x1008FF69, 'XF86OfficeHome': 0x1008FF6A, 'XF86Open': 0x1008FF6B, 'XF86Option': 0x1008FF6C, 'XF86Paste': 0x1008FF6D, 'XF86Phone': 0x1008FF6E, 'XF86Q': 0x1008FF70, 'XF86Reply': 0x1008FF72, 'XF86Reload': 0x1008FF73, 'XF86RotateWindows': 0x1008FF74, 'XF86RotationPB': 0x1008FF75, 'XF86RotationKB': 0x1008FF76, 'XF86Save': 0x1008FF77, 'XF86ScrollUp': 0x1008FF78, 'XF86ScrollDown': 0x1008FF79, 'XF86ScrollClick': 0x1008FF7A, 'XF86Send': 0x1008FF7B, 'XF86Spell': 0x1008FF7C, 'XF86SplitScreen': 0x1008FF7D, 'XF86Support': 0x1008FF7E, 'XF86TaskPane': 0x1008FF7F, 'XF86Terminal': 0x1008FF80, 'XF86Tools': 0x1008FF81, 'XF86Travel': 0x1008FF82, 'XF86UserPB': 0x1008FF84, 'XF86User1KB': 0x1008FF85, 'XF86User2KB': 0x1008FF86, 'XF86Video': 0x1008FF87, 'XF86WheelButton': 0x1008FF88, 'XF86Word': 0x1008FF89, 'XF86Xfer': 0x1008FF8A, 'XF86ZoomIn': 0x1008FF8B, 'XF86ZoomOut': 0x1008FF8C, 'XF86Away': 0x1008FF8D, 'XF86Messenger': 0x1008FF8E, 'XF86WebCam': 0x1008FF8F, 'XF86MailForward': 0x1008FF90, 'XF86Pictures': 0x1008FF91, 'XF86Music': 0x1008FF92, 'XF86Battery': 0x1008FF93, 'XF86Bluetooth': 0x1008FF94, 'XF86WLAN': 0x1008FF95, 'XF86UWB': 0x1008FF96, 'XF86AudioForward': 0x1008FF97, 'XF86AudioRepeat': 0x1008FF98, 'XF86AudioRandomPlay': 0x1008FF99, 'XF86Subtitle': 0x1008FF9A, 'XF86AudioCycleTrack': 0x1008FF9B, 'XF86CycleAngle': 0x1008FF9C, 'XF86FrameBack': 0x1008FF9D, 'XF86FrameForward': 0x1008FF9E, 'XF86Time': 0x1008FF9F, 'XF86Select': 0x1008FFA0, 'XF86View': 0x1008FFA1, 'XF86TopMenu': 0x1008FFA2, 'XF86Red': 0x1008FFA3, 'XF86Green': 0x1008FFA4, 'XF86Yellow': 0x1008FFA5, 'XF86Blue': 0x1008FFA6, 'XF86Suspend': 0x1008FFA7, 'XF86Hibernate': 0x1008FFA8, 'XF86TouchpadToggle': 0x1008FFA9, 'XF86TouchpadOn': 0x1008FFB0, 'XF86TouchpadOff': 0x1008FFB1, 'XF86AudioMicMute': 0x1008FFB2, 'XF86Switch_VT_1': 0x1008FE01, 'XF86Switch_VT_2': 0x1008FE02, 'XF86Switch_VT_3': 0x1008FE03, 'XF86Switch_VT_4': 0x1008FE04, 'XF86Switch_VT_5': 0x1008FE05, 'XF86Switch_VT_6': 0x1008FE06, 'XF86Switch_VT_7': 0x1008FE07, 'XF86Switch_VT_8': 0x1008FE08, 'XF86Switch_VT_9': 0x1008FE09, 'XF86Switch_VT_10': 0x1008FE0A, 'XF86Switch_VT_11': 0x1008FE0B, 'XF86Switch_VT_12': 0x1008FE0C, 'XF86Ungrab': 0x1008FE20, 'XF86ClearGrab': 0x1008FE21, 'XF86Next_VMode': 0x1008FE22, 'XF86Prev_VMode': 0x1008FE23, 'XF86LogWindowTree': 0x1008FE24, 'XF86LogGrabInfo': 0x1008FE25, 'VoidSymbol': 0xffffff, 'BackSpace': 0xff08, 'Tab': 0xff09, 'Linefeed': 0xff0a, 'Clear': 0xff0b, 'Return': 0xff0d, 'Pause': 0xff13, 'Scroll_Lock': 0xff14, 'Sys_Req': 0xff15, 'Escape': 0xff1b, 'Delete': 0xffff, 'Multi_key': 0xff20, 'Codeinput': 0xff37, 'SingleCandidate': 0xff3c, 'MultipleCandidate': 0xff3d, 'PreviousCandidate': 0xff3e, 'Kanji': 0xff21, 'Muhenkan': 0xff22, 'Henkan_Mode': 0xff23, 'Henkan': 0xff23, 'Romaji': 0xff24, 'Hiragana': 0xff25, 'Katakana': 0xff26, 'Hiragana_Katakana': 0xff27, 'Zenkaku': 0xff28, 'Hankaku': 0xff29, 'Zenkaku_Hankaku': 0xff2a, 'Touroku': 0xff2b, 'Massyo': 0xff2c, 'Kana_Lock': 0xff2d, 'Kana_Shift': 0xff2e, 'Eisu_Shift': 0xff2f, 'Eisu_toggle': 0xff30, 'Kanji_Bangou': 0xff37, 'Zen_Koho': 0xff3d, 'Mae_Koho': 0xff3e, 'Home': 0xff50, 'Left': 0xff51, 'Up': 0xff52, 'Right': 0xff53, 'Down': 0xff54, 'Prior': 0xff55, 'Page_Up': 0xff55, 'Next': 0xff56, 'Page_Down': 0xff56, 'End': 0xff57, 'Begin': 0xff58, 'Select': 0xff60, 'Print': 0xff61, 'Execute': 0xff62, 'Insert': 0xff63, 'Undo': 0xff65, 'Redo': 0xff66, 'Menu': 0xff67, 'Find': 0xff68, 'Cancel': 0xff69, 'Help': 0xff6a, 'Break': 0xff6b, 'Mode_switch': 0xff7e, 'script_switch': 0xff7e, 'Num_Lock': 0xff7f, 'KP_Space': 0xff80, 'KP_Tab': 0xff89, 'KP_Enter': 0xff8d, 'KP_F1': 0xff91, 'KP_F2': 0xff92, 'KP_F3': 0xff93, 'KP_F4': 0xff94, 'KP_Home': 0xff95, 'KP_Left': 0xff96, 'KP_Up': 0xff97, 'KP_Right': 0xff98, 'KP_Down': 0xff99, 'KP_Prior': 0xff9a, 'KP_Page_Up': 0xff9a, 'KP_Next': 0xff9b, 'KP_Page_Down': 0xff9b, 'KP_End': 0xff9c, 'KP_Begin': 0xff9d, 'KP_Insert': 0xff9e, 'KP_Delete': 0xff9f, 'KP_Equal': 0xffbd, 'KP_Multiply': 0xffaa, 'KP_Add': 0xffab, 'KP_Separator': 0xffac, 'KP_Subtract': 0xffad, 'KP_Decimal': 0xffae, 'KP_Divide': 0xffaf, 'KP_0': 0xffb0, 'KP_1': 0xffb1, 'KP_2': 0xffb2, 'KP_3': 0xffb3, 'KP_4': 0xffb4, 'KP_5': 0xffb5, 'KP_6': 0xffb6, 'KP_7': 0xffb7, 'KP_8': 0xffb8, 'KP_9': 0xffb9, 'F1': 0xffbe, 'F2': 0xffbf, 'F3': 0xffc0, 'F4': 0xffc1, 'F5': 0xffc2, 'F6': 0xffc3, 'F7': 0xffc4, 'F8': 0xffc5, 'F9': 0xffc6, 'F10': 0xffc7, 'F11': 0xffc8, 'L1': 0xffc8, 'F12': 0xffc9, 'L2': 0xffc9, 'F13': 0xffca, 'L3': 0xffca, 'F14': 0xffcb, 'L4': 0xffcb, 'F15': 0xffcc, 'L5': 0xffcc, 'F16': 0xffcd, 'L6': 0xffcd, 'F17': 0xffce, 'L7': 0xffce, 'F18': 0xffcf, 'L8': 0xffcf, 'F19': 0xffd0, 'L9': 0xffd0, 'F20': 0xffd1, 'L10': 0xffd1, 'F21': 0xffd2, 'R1': 0xffd2, 'F22': 0xffd3, 'R2': 0xffd3, 'F23': 0xffd4, 'R3': 0xffd4, 'F24': 0xffd5, 'R4': 0xffd5, 'F25': 0xffd6, 'R5': 0xffd6, 'F26': 0xffd7, 'R6': 0xffd7, 'F27': 0xffd8, 'R7': 0xffd8, 'F28': 0xffd9, 'R8': 0xffd9, 'F29': 0xffda, 'R9': 0xffda, 'F30': 0xffdb, 'R10': 0xffdb, 'F31': 0xffdc, 'R11': 0xffdc, 'F32': 0xffdd, 'R12': 0xffdd, 'F33': 0xffde, 'R13': 0xffde, 'F34': 0xffdf, 'R14': 0xffdf, 'F35': 0xffe0, 'R15': 0xffe0, 'Shift_L': 0xffe1, 'Shift_R': 0xffe2, 'Control_L': 0xffe3, 'Control_R': 0xffe4, 'Caps_Lock': 0xffe5, 'Shift_Lock': 0xffe6, 'Meta_L': 0xffe7, 'Meta_R': 0xffe8, 'Alt_L': 0xffe9, 'Alt_R': 0xffea, 'Super_L': 0xffeb, 'Super_R': 0xffec, 'Hyper_L': 0xffed, 'Hyper_R': 0xffee, 'ISO_Lock': 0xfe01, 'ISO_Level2_Latch': 0xfe02, 'ISO_Level3_Shift': 0xfe03, 'ISO_Level3_Latch': 0xfe04, 'ISO_Level3_Lock': 0xfe05, 'ISO_Level5_Shift': 0xfe11, 'ISO_Level5_Latch': 0xfe12, 'ISO_Level5_Lock': 0xfe13, 'ISO_Group_Shift': 0xff7e, 'ISO_Group_Latch': 0xfe06, 'ISO_Group_Lock': 0xfe07, 'ISO_Next_Group': 0xfe08, 'ISO_Next_Group_Lock': 0xfe09, 'ISO_Prev_Group': 0xfe0a, 'ISO_Prev_Group_Lock': 0xfe0b, 'ISO_First_Group': 0xfe0c, 'ISO_First_Group_Lock': 0xfe0d, 'ISO_Last_Group': 0xfe0e, 'ISO_Last_Group_Lock': 0xfe0f, 'ISO_Left_Tab': 0xfe20, 'ISO_Move_Line_Up': 0xfe21, 'ISO_Move_Line_Down': 0xfe22, 'ISO_Partial_Line_Up': 0xfe23, 'ISO_Partial_Line_Down': 0xfe24, 'ISO_Partial_Space_Left': 0xfe25, 'ISO_Partial_Space_Right': 0xfe26, 'ISO_Set_Margin_Left': 0xfe27, 'ISO_Set_Margin_Right': 0xfe28, 'ISO_Release_Margin_Left': 0xfe29, 'ISO_Release_Margin_Right': 0xfe2a, 'ISO_Release_Both_Margins': 0xfe2b, 'ISO_Fast_Cursor_Left': 0xfe2c, 'ISO_Fast_Cursor_Right': 0xfe2d, 'ISO_Fast_Cursor_Up': 0xfe2e, 'ISO_Fast_Cursor_Down': 0xfe2f, 'ISO_Continuous_Underline': 0xfe30, 'ISO_Discontinuous_Underline': 0xfe31, 'ISO_Emphasize': 0xfe32, 'ISO_Center_Object': 0xfe33, 'ISO_Enter': 0xfe34, 'dead_grave': 0xfe50, 'dead_acute': 0xfe51, 'dead_circumflex': 0xfe52, 'dead_tilde': 0xfe53, 'dead_perispomeni': 0xfe53, 'dead_macron': 0xfe54, 'dead_breve': 0xfe55, 'dead_abovedot': 0xfe56, 'dead_diaeresis': 0xfe57, 'dead_abovering': 0xfe58, 'dead_doubleacute': 0xfe59, 'dead_caron': 0xfe5a, 'dead_cedilla': 0xfe5b, 'dead_ogonek': 0xfe5c, 'dead_iota': 0xfe5d, 'dead_voiced_sound': 0xfe5e, 'dead_semivoiced_sound': 0xfe5f, 'dead_belowdot': 0xfe60, 'dead_hook': 0xfe61, 'dead_horn': 0xfe62, 'dead_stroke': 0xfe63, 'dead_abovecomma': 0xfe64, 'dead_psili': 0xfe64, 'dead_abovereversedcomma': 0xfe65, 'dead_dasia': 0xfe65, 'dead_doublegrave': 0xfe66, 'dead_belowring': 0xfe67, 'dead_belowmacron': 0xfe68, 'dead_belowcircumflex': 0xfe69, 'dead_belowtilde': 0xfe6a, 'dead_belowbreve': 0xfe6b, 'dead_belowdiaeresis': 0xfe6c, 'dead_invertedbreve': 0xfe6d, 'dead_belowcomma': 0xfe6e, 'dead_currency': 0xfe6f, 'dead_a': 0xfe80, 'dead_A': 0xfe81, 'dead_e': 0xfe82, 'dead_E': 0xfe83, 'dead_i': 0xfe84, 'dead_I': 0xfe85, 'dead_o': 0xfe86, 'dead_O': 0xfe87, 'dead_u': 0xfe88, 'dead_U': 0xfe89, 'dead_small_schwa': 0xfe8a, 'dead_capital_schwa': 0xfe8b, 'First_Virtual_Screen': 0xfed0, 'Prev_Virtual_Screen': 0xfed1, 'Next_Virtual_Screen': 0xfed2, 'Last_Virtual_Screen': 0xfed4, 'Terminate_Server': 0xfed5, 'AccessX_Enable': 0xfe70, 'AccessX_Feedback_Enable': 0xfe71, 'RepeatKeys_Enable': 0xfe72, 'SlowKeys_Enable': 0xfe73, 'BounceKeys_Enable': 0xfe74, 'StickyKeys_Enable': 0xfe75, 'MouseKeys_Enable': 0xfe76, 'MouseKeys_Accel_Enable': 0xfe77, 'Overlay1_Enable': 0xfe78, 'Overlay2_Enable': 0xfe79, 'AudibleBell_Enable': 0xfe7a, 'Pointer_Left': 0xfee0, 'Pointer_Right': 0xfee1, 'Pointer_Up': 0xfee2, 'Pointer_Down': 0xfee3, 'Pointer_UpLeft': 0xfee4, 'Pointer_UpRight': 0xfee5, 'Pointer_DownLeft': 0xfee6, 'Pointer_DownRight': 0xfee7, 'Pointer_Button_Dflt': 0xfee8, 'Pointer_Button1': 0xfee9, 'Pointer_Button2': 0xfeea, 'Pointer_Button3': 0xfeeb, 'Pointer_Button4': 0xfeec, 'Pointer_Button5': 0xfeed, 'Pointer_DblClick_Dflt': 0xfeee, 'Pointer_DblClick1': 0xfeef, 'Pointer_DblClick2': 0xfef0, 'Pointer_DblClick3': 0xfef1, 'Pointer_DblClick4': 0xfef2, 'Pointer_DblClick5': 0xfef3, 'Pointer_Drag_Dflt': 0xfef4, 'Pointer_Drag1': 0xfef5, 'Pointer_Drag2': 0xfef6, 'Pointer_Drag3': 0xfef7, 'Pointer_Drag4': 0xfef8, 'Pointer_Drag5': 0xfefd, 'Pointer_EnableKeys': 0xfef9, 'Pointer_Accelerate': 0xfefa, 'Pointer_DfltBtnNext': 0xfefb, 'Pointer_DfltBtnPrev': 0xfefc, '3270_Duplicate': 0xfd01, '3270_FieldMark': 0xfd02, '3270_Right2': 0xfd03, '3270_Left2': 0xfd04, '3270_BackTab': 0xfd05, '3270_EraseEOF': 0xfd06, '3270_EraseInput': 0xfd07, '3270_Reset': 0xfd08, '3270_Quit': 0xfd09, '3270_PA1': 0xfd0a, '3270_PA2': 0xfd0b, '3270_PA3': 0xfd0c, '3270_Test': 0xfd0d, '3270_Attn': 0xfd0e, '3270_CursorBlink': 0xfd0f, '3270_AltCursor': 0xfd10, '3270_KeyClick': 0xfd11, '3270_Jump': 0xfd12, '3270_Ident': 0xfd13, '3270_Rule': 0xfd14, '3270_Copy': 0xfd15, '3270_Play': 0xfd16, '3270_Setup': 0xfd17, '3270_Record': 0xfd18, '3270_ChangeScreen': 0xfd19, '3270_DeleteWord': 0xfd1a, '3270_ExSelect': 0xfd1b, '3270_CursorSelect': 0xfd1c, '3270_PrintScreen': 0xfd1d, '3270_Enter': 0xfd1e, 'space': 0x0020, 'exclam': 0x0021, 'quotedbl': 0x0022, 'numbersign': 0x0023, 'dollar': 0x0024, 'percent': 0x0025, 'ampersand': 0x0026, 'apostrophe': 0x0027, 'quoteright': 0x0027, 'parenleft': 0x0028, 'parenright': 0x0029, 'asterisk': 0x002a, 'plus': 0x002b, 'comma': 0x002c, 'minus': 0x002d, 'period': 0x002e, 'slash': 0x002f, '0': 0x0030, '1': 0x0031, '2': 0x0032, '3': 0x0033, '4': 0x0034, '5': 0x0035, '6': 0x0036, '7': 0x0037, '8': 0x0038, '9': 0x0039, 'colon': 0x003a, 'semicolon': 0x003b, 'less': 0x003c, 'equal': 0x003d, 'greater': 0x003e, 'question': 0x003f, 'at': 0x0040, 'A': 0x0041, 'B': 0x0042, 'C': 0x0043, 'D': 0x0044, 'E': 0x0045, 'F': 0x0046, 'G': 0x0047, 'H': 0x0048, 'I': 0x0049, 'J': 0x004a, 'K': 0x004b, 'L': 0x004c, 'M': 0x004d, 'N': 0x004e, 'O': 0x004f, 'P': 0x0050, 'Q': 0x0051, 'R': 0x0052, 'S': 0x0053, 'T': 0x0054, 'U': 0x0055, 'V': 0x0056, 'W': 0x0057, 'X': 0x0058, 'Y': 0x0059, 'Z': 0x005a, 'bracketleft': 0x005b, 'backslash': 0x005c, 'bracketright': 0x005d, 'asciicircum': 0x005e, 'underscore': 0x005f, 'grave': 0x0060, 'quoteleft': 0x0060, 'a': 0x0061, 'b': 0x0062, 'c': 0x0063, 'd': 0x0064, 'e': 0x0065, 'f': 0x0066, 'g': 0x0067, 'h': 0x0068, 'i': 0x0069, 'j': 0x006a, 'k': 0x006b, 'l': 0x006c, 'm': 0x006d, 'n': 0x006e, 'o': 0x006f, 'p': 0x0070, 'q': 0x0071, 'r': 0x0072, 's': 0x0073, 't': 0x0074, 'u': 0x0075, 'v': 0x0076, 'w': 0x0077, 'x': 0x0078, 'y': 0x0079, 'z': 0x007a, 'braceleft': 0x007b, 'bar': 0x007c, 'braceright': 0x007d, 'asciitilde': 0x007e, 'nobreakspace': 0x00a0, 'exclamdown': 0x00a1, 'cent': 0x00a2, 'sterling': 0x00a3, 'currency': 0x00a4, 'yen': 0x00a5, 'brokenbar': 0x00a6, 'section': 0x00a7, 'diaeresis': 0x00a8, 'copyright': 0x00a9, 'ordfeminine': 0x00aa, 'guillemotleft': 0x00ab, 'notsign': 0x00ac, 'hyphen': 0x00ad, 'registered': 0x00ae, 'macron': 0x00af, 'degree': 0x00b0, 'plusminus': 0x00b1, 'twosuperior': 0x00b2, 'threesuperior': 0x00b3, 'acute': 0x00b4, 'mu': 0x00b5, 'paragraph': 0x00b6, 'periodcentered': 0x00b7, 'cedilla': 0x00b8, 'onesuperior': 0x00b9, 'masculine': 0x00ba, 'guillemotright': 0x00bb, 'onequarter': 0x00bc, 'onehalf': 0x00bd, 'threequarters': 0x00be, 'questiondown': 0x00bf, 'Agrave': 0x00c0, 'Aacute': 0x00c1, 'Acircumflex': 0x00c2, 'Atilde': 0x00c3, 'Adiaeresis': 0x00c4, 'Aring': 0x00c5, 'AE': 0x00c6, 'Ccedilla': 0x00c7, 'Egrave': 0x00c8, 'Eacute': 0x00c9, 'Ecircumflex': 0x00ca, 'Ediaeresis': 0x00cb, 'Igrave': 0x00cc, 'Iacute': 0x00cd, 'Icircumflex': 0x00ce, 'Idiaeresis': 0x00cf, 'ETH': 0x00d0, 'Eth': 0x00d0, 'Ntilde': 0x00d1, 'Ograve': 0x00d2, 'Oacute': 0x00d3, 'Ocircumflex': 0x00d4, 'Otilde': 0x00d5, 'Odiaeresis': 0x00d6, 'multiply': 0x00d7, 'Oslash': 0x00d8, 'Ooblique': 0x00d8, 'Ugrave': 0x00d9, 'Uacute': 0x00da, 'Ucircumflex': 0x00db, 'Udiaeresis': 0x00dc, 'Yacute': 0x00dd, 'THORN': 0x00de, 'Thorn': 0x00de, 'ssharp': 0x00df, 'agrave': 0x00e0, 'aacute': 0x00e1, 'acircumflex': 0x00e2, 'atilde': 0x00e3, 'adiaeresis': 0x00e4, 'aring': 0x00e5, 'ae': 0x00e6, 'ccedilla': 0x00e7, 'egrave': 0x00e8, 'eacute': 0x00e9, 'ecircumflex': 0x00ea, 'ediaeresis': 0x00eb, 'igrave': 0x00ec, 'iacute': 0x00ed, 'icircumflex': 0x00ee, 'idiaeresis': 0x00ef, 'eth': 0x00f0, 'ntilde': 0x00f1, 'ograve': 0x00f2, 'oacute': 0x00f3, 'ocircumflex': 0x00f4, 'otilde': 0x00f5, 'odiaeresis': 0x00f6, 'division': 0x00f7, 'oslash': 0x00f8, 'ooblique': 0x00f8, 'ugrave': 0x00f9, 'uacute': 0x00fa, 'ucircumflex': 0x00fb, 'udiaeresis': 0x00fc, 'yacute': 0x00fd, 'thorn': 0x00fe, 'ydiaeresis': 0x00ff, 'Aogonek': 0x01a1, 'breve': 0x01a2, 'Lstroke': 0x01a3, 'Lcaron': 0x01a5, 'Sacute': 0x01a6, 'Scaron': 0x01a9, 'Scedilla': 0x01aa, 'Tcaron': 0x01ab, 'Zacute': 0x01ac, 'Zcaron': 0x01ae, 'Zabovedot': 0x01af, 'aogonek': 0x01b1, 'ogonek': 0x01b2, 'lstroke': 0x01b3, 'lcaron': 0x01b5, 'sacute': 0x01b6, 'caron': 0x01b7, 'scaron': 0x01b9, 'scedilla': 0x01ba, 'tcaron': 0x01bb, 'zacute': 0x01bc, 'doubleacute': 0x01bd, 'zcaron': 0x01be, 'zabovedot': 0x01bf, 'Racute': 0x01c0, 'Abreve': 0x01c3, 'Lacute': 0x01c5, 'Cacute': 0x01c6, 'Ccaron': 0x01c8, 'Eogonek': 0x01ca, 'Ecaron': 0x01cc, 'Dcaron': 0x01cf, 'Dstroke': 0x01d0, 'Nacute': 0x01d1, 'Ncaron': 0x01d2, 'Odoubleacute': 0x01d5, 'Rcaron': 0x01d8, 'Uring': 0x01d9, 'Udoubleacute': 0x01db, 'Tcedilla': 0x01de, 'racute': 0x01e0, 'abreve': 0x01e3, 'lacute': 0x01e5, 'cacute': 0x01e6, 'ccaron': 0x01e8, 'eogonek': 0x01ea, 'ecaron': 0x01ec, 'dcaron': 0x01ef, 'dstroke': 0x01f0, 'nacute': 0x01f1, 'ncaron': 0x01f2, 'odoubleacute': 0x01f5, 'udoubleacute': 0x01fb, 'rcaron': 0x01f8, 'uring': 0x01f9, 'tcedilla': 0x01fe, 'abovedot': 0x01ff, 'Hstroke': 0x02a1, 'Hcircumflex': 0x02a6, 'Iabovedot': 0x02a9, 'Gbreve': 0x02ab, 'Jcircumflex': 0x02ac, 'hstroke': 0x02b1, 'hcircumflex': 0x02b6, 'idotless': 0x02b9, 'gbreve': 0x02bb, 'jcircumflex': 0x02bc, 'Cabovedot': 0x02c5, 'Ccircumflex': 0x02c6, 'Gabovedot': 0x02d5, 'Gcircumflex': 0x02d8, 'Ubreve': 0x02dd, 'Scircumflex': 0x02de, 'cabovedot': 0x02e5, 'ccircumflex': 0x02e6, 'gabovedot': 0x02f5, 'gcircumflex': 0x02f8, 'ubreve': 0x02fd, 'scircumflex': 0x02fe, 'kra': 0x03a2, 'kappa': 0x03a2, 'Rcedilla': 0x03a3, 'Itilde': 0x03a5, 'Lcedilla': 0x03a6, 'Emacron': 0x03aa, 'Gcedilla': 0x03ab, 'Tslash': 0x03ac, 'rcedilla': 0x03b3, 'itilde': 0x03b5, 'lcedilla': 0x03b6, 'emacron': 0x03ba, 'gcedilla': 0x03bb, 'tslash': 0x03bc, 'ENG': 0x03bd, 'eng': 0x03bf, 'Amacron': 0x03c0, 'Iogonek': 0x03c7, 'Eabovedot': 0x03cc, 'Imacron': 0x03cf, 'Ncedilla': 0x03d1, 'Omacron': 0x03d2, 'Kcedilla': 0x03d3, 'Uogonek': 0x03d9, 'Utilde': 0x03dd, 'Umacron': 0x03de, 'amacron': 0x03e0, 'iogonek': 0x03e7, 'eabovedot': 0x03ec, 'imacron': 0x03ef, 'ncedilla': 0x03f1, 'omacron': 0x03f2, 'kcedilla': 0x03f3, 'uogonek': 0x03f9, 'utilde': 0x03fd, 'umacron': 0x03fe, 'Babovedot': 0x1001e02, 'babovedot': 0x1001e03, 'Dabovedot': 0x1001e0a, 'Wgrave': 0x1001e80, 'Wacute': 0x1001e82, 'dabovedot': 0x1001e0b, 'Ygrave': 0x1001ef2, 'Fabovedot': 0x1001e1e, 'fabovedot': 0x1001e1f, 'Mabovedot': 0x1001e40, 'mabovedot': 0x1001e41, 'Pabovedot': 0x1001e56, 'wgrave': 0x1001e81, 'pabovedot': 0x1001e57, 'wacute': 0x1001e83, 'Sabovedot': 0x1001e60, 'ygrave': 0x1001ef3, 'Wdiaeresis': 0x1001e84, 'wdiaeresis': 0x1001e85, 'sabovedot': 0x1001e61, 'Wcircumflex': 0x1000174, 'Tabovedot': 0x1001e6a, 'Ycircumflex': 0x1000176, 'wcircumflex': 0x1000175, 'tabovedot': 0x1001e6b, 'ycircumflex': 0x1000177, 'OE': 0x13bc, 'oe': 0x13bd, 'Ydiaeresis': 0x13be, 'overline': 0x047e, 'kana_fullstop': 0x04a1, 'kana_openingbracket': 0x04a2, 'kana_closingbracket': 0x04a3, 'kana_comma': 0x04a4, 'kana_conjunctive': 0x04a5, 'kana_middledot': 0x04a5, 'kana_WO': 0x04a6, 'kana_a': 0x04a7, 'kana_i': 0x04a8, 'kana_u': 0x04a9, 'kana_e': 0x04aa, 'kana_o': 0x04ab, 'kana_ya': 0x04ac, 'kana_yu': 0x04ad, 'kana_yo': 0x04ae, 'kana_tsu': 0x04af, 'kana_tu': 0x04af, 'prolongedsound': 0x04b0, 'kana_A': 0x04b1, 'kana_I': 0x04b2, 'kana_U': 0x04b3, 'kana_E': 0x04b4, 'kana_O': 0x04b5, 'kana_KA': 0x04b6, 'kana_KI': 0x04b7, 'kana_KU': 0x04b8, 'kana_KE': 0x04b9, 'kana_KO': 0x04ba, 'kana_SA': 0x04bb, 'kana_SHI': 0x04bc, 'kana_SU': 0x04bd, 'kana_SE': 0x04be, 'kana_SO': 0x04bf, 'kana_TA': 0x04c0, 'kana_CHI': 0x04c1, 'kana_TI': 0x04c1, 'kana_TSU': 0x04c2, 'kana_TU': 0x04c2, 'kana_TE': 0x04c3, 'kana_TO': 0x04c4, 'kana_NA': 0x04c5, 'kana_NI': 0x04c6, 'kana_NU': 0x04c7, 'kana_NE': 0x04c8, 'kana_NO': 0x04c9, 'kana_HA': 0x04ca, 'kana_HI': 0x04cb, 'kana_FU': 0x04cc, 'kana_HU': 0x04cc, 'kana_HE': 0x04cd, 'kana_HO': 0x04ce, 'kana_MA': 0x04cf, 'kana_MI': 0x04d0, 'kana_MU': 0x04d1, 'kana_ME': 0x04d2, 'kana_MO': 0x04d3, 'kana_YA': 0x04d4, 'kana_YU': 0x04d5, 'kana_YO': 0x04d6, 'kana_RA': 0x04d7, 'kana_RI': 0x04d8, 'kana_RU': 0x04d9, 'kana_RE': 0x04da, 'kana_RO': 0x04db, 'kana_WA': 0x04dc, 'kana_N': 0x04dd, 'voicedsound': 0x04de, 'semivoicedsound': 0x04df, 'kana_switch': 0xff7e, 'Farsi_0': 0x10006f0, 'Farsi_1': 0x10006f1, 'Farsi_2': 0x10006f2, 'Farsi_3': 0x10006f3, 'Farsi_4': 0x10006f4, 'Farsi_5': 0x10006f5, 'Farsi_6': 0x10006f6, 'Farsi_7': 0x10006f7, 'Farsi_8': 0x10006f8, 'Farsi_9': 0x10006f9, 'Arabic_percent': 0x100066a, 'Arabic_superscript_alef': 0x1000670, 'Arabic_tteh': 0x1000679, 'Arabic_peh': 0x100067e, 'Arabic_tcheh': 0x1000686, 'Arabic_ddal': 0x1000688, 'Arabic_rreh': 0x1000691, 'Arabic_comma': 0x05ac, 'Arabic_fullstop': 0x10006d4, 'Arabic_0': 0x1000660, 'Arabic_1': 0x1000661, 'Arabic_2': 0x1000662, 'Arabic_3': 0x1000663, 'Arabic_4': 0x1000664, 'Arabic_5': 0x1000665, 'Arabic_6': 0x1000666, 'Arabic_7': 0x1000667, 'Arabic_8': 0x1000668, 'Arabic_9': 0x1000669, 'Arabic_semicolon': 0x05bb, 'Arabic_question_mark': 0x05bf, 'Arabic_hamza': 0x05c1, 'Arabic_maddaonalef': 0x05c2, 'Arabic_hamzaonalef': 0x05c3, 'Arabic_hamzaonwaw': 0x05c4, 'Arabic_hamzaunderalef': 0x05c5, 'Arabic_hamzaonyeh': 0x05c6, 'Arabic_alef': 0x05c7, 'Arabic_beh': 0x05c8, 'Arabic_tehmarbuta': 0x05c9, 'Arabic_teh': 0x05ca, 'Arabic_theh': 0x05cb, 'Arabic_jeem': 0x05cc, 'Arabic_hah': 0x05cd, 'Arabic_khah': 0x05ce, 'Arabic_dal': 0x05cf, 'Arabic_thal': 0x05d0, 'Arabic_ra': 0x05d1, 'Arabic_zain': 0x05d2, 'Arabic_seen': 0x05d3, 'Arabic_sheen': 0x05d4, 'Arabic_sad': 0x05d5, 'Arabic_dad': 0x05d6, 'Arabic_tah': 0x05d7, 'Arabic_zah': 0x05d8, 'Arabic_ain': 0x05d9, 'Arabic_ghain': 0x05da, 'Arabic_tatweel': 0x05e0, 'Arabic_feh': 0x05e1, 'Arabic_qaf': 0x05e2, 'Arabic_kaf': 0x05e3, 'Arabic_lam': 0x05e4, 'Arabic_meem': 0x05e5, 'Arabic_noon': 0x05e6, 'Arabic_ha': 0x05e7, 'Arabic_heh': 0x05e7, 'Arabic_waw': 0x05e8, 'Arabic_alefmaksura': 0x05e9, 'Arabic_yeh': 0x05ea, 'Arabic_fathatan': 0x05eb, 'Arabic_dammatan': 0x05ec, 'Arabic_kasratan': 0x05ed, 'Arabic_fatha': 0x05ee, 'Arabic_damma': 0x05ef, 'Arabic_kasra': 0x05f0, 'Arabic_shadda': 0x05f1, 'Arabic_sukun': 0x05f2, 'Arabic_madda_above': 0x1000653, 'Arabic_hamza_above': 0x1000654, 'Arabic_hamza_below': 0x1000655, 'Arabic_jeh': 0x1000698, 'Arabic_veh': 0x10006a4, 'Arabic_keheh': 0x10006a9, 'Arabic_gaf': 0x10006af, 'Arabic_noon_ghunna': 0x10006ba, 'Arabic_heh_doachashmee': 0x10006be, 'Farsi_yeh': 0x10006cc, 'Arabic_farsi_yeh': 0x10006cc, 'Arabic_yeh_baree': 0x10006d2, 'Arabic_heh_goal': 0x10006c1, 'Arabic_switch': 0xff7e, 'Cyrillic_GHE_bar': 0x1000492, 'Cyrillic_ghe_bar': 0x1000493, 'Cyrillic_ZHE_descender': 0x1000496, 'Cyrillic_zhe_descender': 0x1000497, 'Cyrillic_KA_descender': 0x100049a, 'Cyrillic_ka_descender': 0x100049b, 'Cyrillic_KA_vertstroke': 0x100049c, 'Cyrillic_ka_vertstroke': 0x100049d, 'Cyrillic_EN_descender': 0x10004a2, 'Cyrillic_en_descender': 0x10004a3, 'Cyrillic_U_straight': 0x10004ae, 'Cyrillic_u_straight': 0x10004af, 'Cyrillic_U_straight_bar': 0x10004b0, 'Cyrillic_u_straight_bar': 0x10004b1, 'Cyrillic_HA_descender': 0x10004b2, 'Cyrillic_ha_descender': 0x10004b3, 'Cyrillic_CHE_descender': 0x10004b6, 'Cyrillic_che_descender': 0x10004b7, 'Cyrillic_CHE_vertstroke': 0x10004b8, 'Cyrillic_che_vertstroke': 0x10004b9, 'Cyrillic_SHHA': 0x10004ba, 'Cyrillic_shha': 0x10004bb, 'Cyrillic_SCHWA': 0x10004d8, 'Cyrillic_schwa': 0x10004d9, 'Cyrillic_I_macron': 0x10004e2, 'Cyrillic_i_macron': 0x10004e3, 'Cyrillic_O_bar': 0x10004e8, 'Cyrillic_o_bar': 0x10004e9, 'Cyrillic_U_macron': 0x10004ee, 'Cyrillic_u_macron': 0x10004ef, 'Serbian_dje': 0x06a1, 'Macedonia_gje': 0x06a2, 'Cyrillic_io': 0x06a3, 'Ukrainian_ie': 0x06a4, 'Ukranian_je': 0x06a4, 'Macedonia_dse': 0x06a5, 'Ukrainian_i': 0x06a6, 'Ukranian_i': 0x06a6, 'Ukrainian_yi': 0x06a7, 'Ukranian_yi': 0x06a7, 'Cyrillic_je': 0x06a8, 'Serbian_je': 0x06a8, 'Cyrillic_lje': 0x06a9, 'Serbian_lje': 0x06a9, 'Cyrillic_nje': 0x06aa, 'Serbian_nje': 0x06aa, 'Serbian_tshe': 0x06ab, 'Macedonia_kje': 0x06ac, 'Ukrainian_ghe_with_upturn': 0x06ad, 'Byelorussian_shortu': 0x06ae, 'Cyrillic_dzhe': 0x06af, 'Serbian_dze': 0x06af, 'numerosign': 0x06b0, 'Serbian_DJE': 0x06b1, 'Macedonia_GJE': 0x06b2, 'Cyrillic_IO': 0x06b3, 'Ukrainian_IE': 0x06b4, 'Ukranian_JE': 0x06b4, 'Macedonia_DSE': 0x06b5, 'Ukrainian_I': 0x06b6, 'Ukranian_I': 0x06b6, 'Ukrainian_YI': 0x06b7, 'Ukranian_YI': 0x06b7, 'Cyrillic_JE': 0x06b8, 'Serbian_JE': 0x06b8, 'Cyrillic_LJE': 0x06b9, 'Serbian_LJE': 0x06b9, 'Cyrillic_NJE': 0x06ba, 'Serbian_NJE': 0x06ba, 'Serbian_TSHE': 0x06bb, 'Macedonia_KJE': 0x06bc, 'Ukrainian_GHE_WITH_UPTURN': 0x06bd, 'Byelorussian_SHORTU': 0x06be, 'Cyrillic_DZHE': 0x06bf, 'Serbian_DZE': 0x06bf, 'Cyrillic_yu': 0x06c0, 'Cyrillic_a': 0x06c1, 'Cyrillic_be': 0x06c2, 'Cyrillic_tse': 0x06c3, 'Cyrillic_de': 0x06c4, 'Cyrillic_ie': 0x06c5, 'Cyrillic_ef': 0x06c6, 'Cyrillic_ghe': 0x06c7, 'Cyrillic_ha': 0x06c8, 'Cyrillic_i': 0x06c9, 'Cyrillic_shorti': 0x06ca, 'Cyrillic_ka': 0x06cb, 'Cyrillic_el': 0x06cc, 'Cyrillic_em': 0x06cd, 'Cyrillic_en': 0x06ce, 'Cyrillic_o': 0x06cf, 'Cyrillic_pe': 0x06d0, 'Cyrillic_ya': 0x06d1, 'Cyrillic_er': 0x06d2, 'Cyrillic_es': 0x06d3, 'Cyrillic_te': 0x06d4, 'Cyrillic_u': 0x06d5, 'Cyrillic_zhe': 0x06d6, 'Cyrillic_ve': 0x06d7, 'Cyrillic_softsign': 0x06d8, 'Cyrillic_yeru': 0x06d9, 'Cyrillic_ze': 0x06da, 'Cyrillic_sha': 0x06db, 'Cyrillic_e': 0x06dc, 'Cyrillic_shcha': 0x06dd, 'Cyrillic_che': 0x06de, 'Cyrillic_hardsign': 0x06df, 'Cyrillic_YU': 0x06e0, 'Cyrillic_A': 0x06e1, 'Cyrillic_BE': 0x06e2, 'Cyrillic_TSE': 0x06e3, 'Cyrillic_DE': 0x06e4, 'Cyrillic_IE': 0x06e5, 'Cyrillic_EF': 0x06e6, 'Cyrillic_GHE': 0x06e7, 'Cyrillic_HA': 0x06e8, 'Cyrillic_I': 0x06e9, 'Cyrillic_SHORTI': 0x06ea, 'Cyrillic_KA': 0x06eb, 'Cyrillic_EL': 0x06ec, 'Cyrillic_EM': 0x06ed, 'Cyrillic_EN': 0x06ee, 'Cyrillic_O': 0x06ef, 'Cyrillic_PE': 0x06f0, 'Cyrillic_YA': 0x06f1, 'Cyrillic_ER': 0x06f2, 'Cyrillic_ES': 0x06f3, 'Cyrillic_TE': 0x06f4, 'Cyrillic_U': 0x06f5, 'Cyrillic_ZHE': 0x06f6, 'Cyrillic_VE': 0x06f7, 'Cyrillic_SOFTSIGN': 0x06f8, 'Cyrillic_YERU': 0x06f9, 'Cyrillic_ZE': 0x06fa, 'Cyrillic_SHA': 0x06fb, 'Cyrillic_E': 0x06fc, 'Cyrillic_SHCHA': 0x06fd, 'Cyrillic_CHE': 0x06fe, 'Cyrillic_HARDSIGN': 0x06ff, 'Greek_ALPHAaccent': 0x07a1, 'Greek_EPSILONaccent': 0x07a2, 'Greek_ETAaccent': 0x07a3, 'Greek_IOTAaccent': 0x07a4, 'Greek_IOTAdieresis': 0x07a5, 'Greek_IOTAdiaeresis': 0x07a5, 'Greek_OMICRONaccent': 0x07a7, 'Greek_UPSILONaccent': 0x07a8, 'Greek_UPSILONdieresis': 0x07a9, 'Greek_OMEGAaccent': 0x07ab, 'Greek_accentdieresis': 0x07ae, 'Greek_horizbar': 0x07af, 'Greek_alphaaccent': 0x07b1, 'Greek_epsilonaccent': 0x07b2, 'Greek_etaaccent': 0x07b3, 'Greek_iotaaccent': 0x07b4, 'Greek_iotadieresis': 0x07b5, 'Greek_iotaaccentdieresis': 0x07b6, 'Greek_omicronaccent': 0x07b7, 'Greek_upsilonaccent': 0x07b8, 'Greek_upsilondieresis': 0x07b9, 'Greek_upsilonaccentdieresis': 0x07ba, 'Greek_omegaaccent': 0x07bb, 'Greek_ALPHA': 0x07c1, 'Greek_BETA': 0x07c2, 'Greek_GAMMA': 0x07c3, 'Greek_DELTA': 0x07c4, 'Greek_EPSILON': 0x07c5, 'Greek_ZETA': 0x07c6, 'Greek_ETA': 0x07c7, 'Greek_THETA': 0x07c8, 'Greek_IOTA': 0x07c9, 'Greek_KAPPA': 0x07ca, 'Greek_LAMDA': 0x07cb, 'Greek_LAMBDA': 0x07cb, 'Greek_MU': 0x07cc, 'Greek_NU': 0x07cd, 'Greek_XI': 0x07ce, 'Greek_OMICRON': 0x07cf, 'Greek_PI': 0x07d0, 'Greek_RHO': 0x07d1, 'Greek_SIGMA': 0x07d2, 'Greek_TAU': 0x07d4, 'Greek_UPSILON': 0x07d5, 'Greek_PHI': 0x07d6, 'Greek_CHI': 0x07d7, 'Greek_PSI': 0x07d8, 'Greek_OMEGA': 0x07d9, 'Greek_alpha': 0x07e1, 'Greek_beta': 0x07e2, 'Greek_gamma': 0x07e3, 'Greek_delta': 0x07e4, 'Greek_epsilon': 0x07e5, 'Greek_zeta': 0x07e6, 'Greek_eta': 0x07e7, 'Greek_theta': 0x07e8, 'Greek_iota': 0x07e9, 'Greek_kappa': 0x07ea, 'Greek_lamda': 0x07eb, 'Greek_lambda': 0x07eb, 'Greek_mu': 0x07ec, 'Greek_nu': 0x07ed, 'Greek_xi': 0x07ee, 'Greek_omicron': 0x07ef, 'Greek_pi': 0x07f0, 'Greek_rho': 0x07f1, 'Greek_sigma': 0x07f2, 'Greek_finalsmallsigma': 0x07f3, 'Greek_tau': 0x07f4, 'Greek_upsilon': 0x07f5, 'Greek_phi': 0x07f6, 'Greek_chi': 0x07f7, 'Greek_psi': 0x07f8, 'Greek_omega': 0x07f9, 'Greek_switch': 0xff7e, 'leftradical': 0x08a1, 'topleftradical': 0x08a2, 'horizconnector': 0x08a3, 'topintegral': 0x08a4, 'botintegral': 0x08a5, 'vertconnector': 0x08a6, 'topleftsqbracket': 0x08a7, 'botleftsqbracket': 0x08a8, 'toprightsqbracket': 0x08a9, 'botrightsqbracket': 0x08aa, 'topleftparens': 0x08ab, 'botleftparens': 0x08ac, 'toprightparens': 0x08ad, 'botrightparens': 0x08ae, 'leftmiddlecurlybrace': 0x08af, 'rightmiddlecurlybrace': 0x08b0, 'topleftsummation': 0x08b1, 'botleftsummation': 0x08b2, 'topvertsummationconnector': 0x08b3, 'botvertsummationconnector': 0x08b4, 'toprightsummation': 0x08b5, 'botrightsummation': 0x08b6, 'rightmiddlesummation': 0x08b7, 'lessthanequal': 0x08bc, 'notequal': 0x08bd, 'greaterthanequal': 0x08be, 'integral': 0x08bf, 'therefore': 0x08c0, 'variation': 0x08c1, 'infinity': 0x08c2, 'nabla': 0x08c5, 'approximate': 0x08c8, 'similarequal': 0x08c9, 'ifonlyif': 0x08cd, 'implies': 0x08ce, 'identical': 0x08cf, 'radical': 0x08d6, 'includedin': 0x08da, 'includes': 0x08db, 'intersection': 0x08dc, 'union': 0x08dd, 'logicaland': 0x08de, 'logicalor': 0x08df, 'partialderivative': 0x08ef, 'function': 0x08f6, 'leftarrow': 0x08fb, 'uparrow': 0x08fc, 'rightarrow': 0x08fd, 'downarrow': 0x08fe, 'blank': 0x09df, 'soliddiamond': 0x09e0, 'checkerboard': 0x09e1, 'ht': 0x09e2, 'ff': 0x09e3, 'cr': 0x09e4, 'lf': 0x09e5, 'nl': 0x09e8, 'vt': 0x09e9, 'lowrightcorner': 0x09ea, 'uprightcorner': 0x09eb, 'upleftcorner': 0x09ec, 'lowleftcorner': 0x09ed, 'crossinglines': 0x09ee, 'horizlinescan1': 0x09ef, 'horizlinescan3': 0x09f0, 'horizlinescan5': 0x09f1, 'horizlinescan7': 0x09f2, 'horizlinescan9': 0x09f3, 'leftt': 0x09f4, 'rightt': 0x09f5, 'bott': 0x09f6, 'topt': 0x09f7, 'vertbar': 0x09f8, 'emspace': 0x0aa1, 'enspace': 0x0aa2, 'em3space': 0x0aa3, 'em4space': 0x0aa4, 'digitspace': 0x0aa5, 'punctspace': 0x0aa6, 'thinspace': 0x0aa7, 'hairspace': 0x0aa8, 'emdash': 0x0aa9, 'endash': 0x0aaa, 'signifblank': 0x0aac, 'ellipsis': 0x0aae, 'doubbaselinedot': 0x0aaf, 'onethird': 0x0ab0, 'twothirds': 0x0ab1, 'onefifth': 0x0ab2, 'twofifths': 0x0ab3, 'threefifths': 0x0ab4, 'fourfifths': 0x0ab5, 'onesixth': 0x0ab6, 'fivesixths': 0x0ab7, 'careof': 0x0ab8, 'figdash': 0x0abb, 'leftanglebracket': 0x0abc, 'decimalpoint': 0x0abd, 'rightanglebracket': 0x0abe, 'marker': 0x0abf, 'oneeighth': 0x0ac3, 'threeeighths': 0x0ac4, 'fiveeighths': 0x0ac5, 'seveneighths': 0x0ac6, 'trademark': 0x0ac9, 'signaturemark': 0x0aca, 'trademarkincircle': 0x0acb, 'leftopentriangle': 0x0acc, 'rightopentriangle': 0x0acd, 'emopencircle': 0x0ace, 'emopenrectangle': 0x0acf, 'leftsinglequotemark': 0x0ad0, 'rightsinglequotemark': 0x0ad1, 'leftdoublequotemark': 0x0ad2, 'rightdoublequotemark': 0x0ad3, 'prescription': 0x0ad4, 'minutes': 0x0ad6, 'seconds': 0x0ad7, 'latincross': 0x0ad9, 'hexagram': 0x0ada, 'filledrectbullet': 0x0adb, 'filledlefttribullet': 0x0adc, 'filledrighttribullet': 0x0add, 'emfilledcircle': 0x0ade, 'emfilledrect': 0x0adf, 'enopencircbullet': 0x0ae0, 'enopensquarebullet': 0x0ae1, 'openrectbullet': 0x0ae2, 'opentribulletup': 0x0ae3, 'opentribulletdown': 0x0ae4, 'openstar': 0x0ae5, 'enfilledcircbullet': 0x0ae6, 'enfilledsqbullet': 0x0ae7, 'filledtribulletup': 0x0ae8, 'filledtribulletdown': 0x0ae9, 'leftpointer': 0x0aea, 'rightpointer': 0x0aeb, 'club': 0x0aec, 'diamond': 0x0aed, 'heart': 0x0aee, 'maltesecross': 0x0af0, 'dagger': 0x0af1, 'doubledagger': 0x0af2, 'checkmark': 0x0af3, 'ballotcross': 0x0af4, 'musicalsharp': 0x0af5, 'musicalflat': 0x0af6, 'malesymbol': 0x0af7, 'femalesymbol': 0x0af8, 'telephone': 0x0af9, 'telephonerecorder': 0x0afa, 'phonographcopyright': 0x0afb, 'caret': 0x0afc, 'singlelowquotemark': 0x0afd, 'doublelowquotemark': 0x0afe, 'cursor': 0x0aff, 'leftcaret': 0x0ba3, 'rightcaret': 0x0ba6, 'downcaret': 0x0ba8, 'upcaret': 0x0ba9, 'overbar': 0x0bc0, 'downtack': 0x0bc2, 'upshoe': 0x0bc3, 'downstile': 0x0bc4, 'underbar': 0x0bc6, 'jot': 0x0bca, 'quad': 0x0bcc, 'uptack': 0x0bce, 'circle': 0x0bcf, 'upstile': 0x0bd3, 'downshoe': 0x0bd6, 'rightshoe': 0x0bd8, 'leftshoe': 0x0bda, 'lefttack': 0x0bdc, 'righttack': 0x0bfc, 'hebrew_doublelowline': 0x0cdf, 'hebrew_aleph': 0x0ce0, 'hebrew_bet': 0x0ce1, 'hebrew_beth': 0x0ce1, 'hebrew_gimel': 0x0ce2, 'hebrew_gimmel': 0x0ce2, 'hebrew_dalet': 0x0ce3, 'hebrew_daleth': 0x0ce3, 'hebrew_he': 0x0ce4, 'hebrew_waw': 0x0ce5, 'hebrew_zain': 0x0ce6, 'hebrew_zayin': 0x0ce6, 'hebrew_chet': 0x0ce7, 'hebrew_het': 0x0ce7, 'hebrew_tet': 0x0ce8, 'hebrew_teth': 0x0ce8, 'hebrew_yod': 0x0ce9, 'hebrew_finalkaph': 0x0cea, 'hebrew_kaph': 0x0ceb, 'hebrew_lamed': 0x0cec, 'hebrew_finalmem': 0x0ced, 'hebrew_mem': 0x0cee, 'hebrew_finalnun': 0x0cef, 'hebrew_nun': 0x0cf0, 'hebrew_samech': 0x0cf1, 'hebrew_samekh': 0x0cf1, 'hebrew_ayin': 0x0cf2, 'hebrew_finalpe': 0x0cf3, 'hebrew_pe': 0x0cf4, 'hebrew_finalzade': 0x0cf5, 'hebrew_finalzadi': 0x0cf5, 'hebrew_zade': 0x0cf6, 'hebrew_zadi': 0x0cf6, 'hebrew_qoph': 0x0cf7, 'hebrew_kuf': 0x0cf7, 'hebrew_resh': 0x0cf8, 'hebrew_shin': 0x0cf9, 'hebrew_taw': 0x0cfa, 'hebrew_taf': 0x0cfa, 'Hebrew_switch': 0xff7e, 'Thai_kokai': 0x0da1, 'Thai_khokhai': 0x0da2, 'Thai_khokhuat': 0x0da3, 'Thai_khokhwai': 0x0da4, 'Thai_khokhon': 0x0da5, 'Thai_khorakhang': 0x0da6, 'Thai_ngongu': 0x0da7, 'Thai_chochan': 0x0da8, 'Thai_choching': 0x0da9, 'Thai_chochang': 0x0daa, 'Thai_soso': 0x0dab, 'Thai_chochoe': 0x0dac, 'Thai_yoying': 0x0dad, 'Thai_dochada': 0x0dae, 'Thai_topatak': 0x0daf, 'Thai_thothan': 0x0db0, 'Thai_thonangmontho': 0x0db1, 'Thai_thophuthao': 0x0db2, 'Thai_nonen': 0x0db3, 'Thai_dodek': 0x0db4, 'Thai_totao': 0x0db5, 'Thai_thothung': 0x0db6, 'Thai_thothahan': 0x0db7, 'Thai_thothong': 0x0db8, 'Thai_nonu': 0x0db9, 'Thai_bobaimai': 0x0dba, 'Thai_popla': 0x0dbb, 'Thai_phophung': 0x0dbc, 'Thai_fofa': 0x0dbd, 'Thai_phophan': 0x0dbe, 'Thai_fofan': 0x0dbf, 'Thai_phosamphao': 0x0dc0, 'Thai_moma': 0x0dc1, 'Thai_yoyak': 0x0dc2, 'Thai_rorua': 0x0dc3, 'Thai_ru': 0x0dc4, 'Thai_loling': 0x0dc5, 'Thai_lu': 0x0dc6, 'Thai_wowaen': 0x0dc7, 'Thai_sosala': 0x0dc8, 'Thai_sorusi': 0x0dc9, 'Thai_sosua': 0x0dca, 'Thai_hohip': 0x0dcb, 'Thai_lochula': 0x0dcc, 'Thai_oang': 0x0dcd, 'Thai_honokhuk': 0x0dce, 'Thai_paiyannoi': 0x0dcf, 'Thai_saraa': 0x0dd0, 'Thai_maihanakat': 0x0dd1, 'Thai_saraaa': 0x0dd2, 'Thai_saraam': 0x0dd3, 'Thai_sarai': 0x0dd4, 'Thai_saraii': 0x0dd5, 'Thai_saraue': 0x0dd6, 'Thai_sarauee': 0x0dd7, 'Thai_sarau': 0x0dd8, 'Thai_sarauu': 0x0dd9, 'Thai_phinthu': 0x0dda, 'Thai_maihanakat_maitho': 0x0dde, 'Thai_baht': 0x0ddf, 'Thai_sarae': 0x0de0, 'Thai_saraae': 0x0de1, 'Thai_sarao': 0x0de2, 'Thai_saraaimaimuan': 0x0de3, 'Thai_saraaimaimalai': 0x0de4, 'Thai_lakkhangyao': 0x0de5, 'Thai_maiyamok': 0x0de6, 'Thai_maitaikhu': 0x0de7, 'Thai_maiek': 0x0de8, 'Thai_maitho': 0x0de9, 'Thai_maitri': 0x0dea, 'Thai_maichattawa': 0x0deb, 'Thai_thanthakhat': 0x0dec, 'Thai_nikhahit': 0x0ded, 'Thai_leksun': 0x0df0, 'Thai_leknung': 0x0df1, 'Thai_leksong': 0x0df2, 'Thai_leksam': 0x0df3, 'Thai_leksi': 0x0df4, 'Thai_lekha': 0x0df5, 'Thai_lekhok': 0x0df6, 'Thai_lekchet': 0x0df7, 'Thai_lekpaet': 0x0df8, 'Thai_lekkao': 0x0df9, 'Hangul': 0xff31, 'Hangul_Start': 0xff32, 'Hangul_End': 0xff33, 'Hangul_Hanja': 0xff34, 'Hangul_Jamo': 0xff35, 'Hangul_Romaja': 0xff36, 'Hangul_Codeinput': 0xff37, 'Hangul_Jeonja': 0xff38, 'Hangul_Banja': 0xff39, 'Hangul_PreHanja': 0xff3a, 'Hangul_PostHanja': 0xff3b, 'Hangul_SingleCandidate': 0xff3c, 'Hangul_MultipleCandidate': 0xff3d, 'Hangul_PreviousCandidate': 0xff3e, 'Hangul_Special': 0xff3f, 'Hangul_switch': 0xff7e, 'Hangul_Kiyeog': 0x0ea1, 'Hangul_SsangKiyeog': 0x0ea2, 'Hangul_KiyeogSios': 0x0ea3, 'Hangul_Nieun': 0x0ea4, 'Hangul_NieunJieuj': 0x0ea5, 'Hangul_NieunHieuh': 0x0ea6, 'Hangul_Dikeud': 0x0ea7, 'Hangul_SsangDikeud': 0x0ea8, 'Hangul_Rieul': 0x0ea9, 'Hangul_RieulKiyeog': 0x0eaa, 'Hangul_RieulMieum': 0x0eab, 'Hangul_RieulPieub': 0x0eac, 'Hangul_RieulSios': 0x0ead, 'Hangul_RieulTieut': 0x0eae, 'Hangul_RieulPhieuf': 0x0eaf, 'Hangul_RieulHieuh': 0x0eb0, 'Hangul_Mieum': 0x0eb1, 'Hangul_Pieub': 0x0eb2, 'Hangul_SsangPieub': 0x0eb3, 'Hangul_PieubSios': 0x0eb4, 'Hangul_Sios': 0x0eb5, 'Hangul_SsangSios': 0x0eb6, 'Hangul_Ieung': 0x0eb7, 'Hangul_Jieuj': 0x0eb8, 'Hangul_SsangJieuj': 0x0eb9, 'Hangul_Cieuc': 0x0eba, 'Hangul_Khieuq': 0x0ebb, 'Hangul_Tieut': 0x0ebc, 'Hangul_Phieuf': 0x0ebd, 'Hangul_Hieuh': 0x0ebe, 'Hangul_A': 0x0ebf, 'Hangul_AE': 0x0ec0, 'Hangul_YA': 0x0ec1, 'Hangul_YAE': 0x0ec2, 'Hangul_EO': 0x0ec3, 'Hangul_E': 0x0ec4, 'Hangul_YEO': 0x0ec5, 'Hangul_YE': 0x0ec6, 'Hangul_O': 0x0ec7, 'Hangul_WA': 0x0ec8, 'Hangul_WAE': 0x0ec9, 'Hangul_OE': 0x0eca, 'Hangul_YO': 0x0ecb, 'Hangul_U': 0x0ecc, 'Hangul_WEO': 0x0ecd, 'Hangul_WE': 0x0ece, 'Hangul_WI': 0x0ecf, 'Hangul_YU': 0x0ed0, 'Hangul_EU': 0x0ed1, 'Hangul_YI': 0x0ed2, 'Hangul_I': 0x0ed3, 'Hangul_J_Kiyeog': 0x0ed4, 'Hangul_J_SsangKiyeog': 0x0ed5, 'Hangul_J_KiyeogSios': 0x0ed6, 'Hangul_J_Nieun': 0x0ed7, 'Hangul_J_NieunJieuj': 0x0ed8, 'Hangul_J_NieunHieuh': 0x0ed9, 'Hangul_J_Dikeud': 0x0eda, 'Hangul_J_Rieul': 0x0edb, 'Hangul_J_RieulKiyeog': 0x0edc, 'Hangul_J_RieulMieum': 0x0edd, 'Hangul_J_RieulPieub': 0x0ede, 'Hangul_J_RieulSios': 0x0edf, 'Hangul_J_RieulTieut': 0x0ee0, 'Hangul_J_RieulPhieuf': 0x0ee1, 'Hangul_J_RieulHieuh': 0x0ee2, 'Hangul_J_Mieum': 0x0ee3, 'Hangul_J_Pieub': 0x0ee4, 'Hangul_J_PieubSios': 0x0ee5, 'Hangul_J_Sios': 0x0ee6, 'Hangul_J_SsangSios': 0x0ee7, 'Hangul_J_Ieung': 0x0ee8, 'Hangul_J_Jieuj': 0x0ee9, 'Hangul_J_Cieuc': 0x0eea, 'Hangul_J_Khieuq': 0x0eeb, 'Hangul_J_Tieut': 0x0eec, 'Hangul_J_Phieuf': 0x0eed, 'Hangul_J_Hieuh': 0x0eee, 'Hangul_RieulYeorinHieuh': 0x0eef, 'Hangul_SunkyeongeumMieum': 0x0ef0, 'Hangul_SunkyeongeumPieub': 0x0ef1, 'Hangul_PanSios': 0x0ef2, 'Hangul_KkogjiDalrinIeung': 0x0ef3, 'Hangul_SunkyeongeumPhieuf': 0x0ef4, 'Hangul_YeorinHieuh': 0x0ef5, 'Hangul_AraeA': 0x0ef6, 'Hangul_AraeAE': 0x0ef7, 'Hangul_J_PanSios': 0x0ef8, 'Hangul_J_KkogjiDalrinIeung': 0x0ef9, 'Hangul_J_YeorinHieuh': 0x0efa, 'Korean_Won': 0x0eff, 'Armenian_ligature_ew': 0x1000587, 'Armenian_full_stop': 0x1000589, 'Armenian_verjaket': 0x1000589, 'Armenian_separation_mark': 0x100055d, 'Armenian_but': 0x100055d, 'Armenian_hyphen': 0x100058a, 'Armenian_yentamna': 0x100058a, 'Armenian_exclam': 0x100055c, 'Armenian_amanak': 0x100055c, 'Armenian_accent': 0x100055b, 'Armenian_shesht': 0x100055b, 'Armenian_question': 0x100055e, 'Armenian_paruyk': 0x100055e, 'Armenian_AYB': 0x1000531, 'Armenian_ayb': 0x1000561, 'Armenian_BEN': 0x1000532, 'Armenian_ben': 0x1000562, 'Armenian_GIM': 0x1000533, 'Armenian_gim': 0x1000563, 'Armenian_DA': 0x1000534, 'Armenian_da': 0x1000564, 'Armenian_YECH': 0x1000535, 'Armenian_yech': 0x1000565, 'Armenian_ZA': 0x1000536, 'Armenian_za': 0x1000566, 'Armenian_E': 0x1000537, 'Armenian_e': 0x1000567, 'Armenian_AT': 0x1000538, 'Armenian_at': 0x1000568, 'Armenian_TO': 0x1000539, 'Armenian_to': 0x1000569, 'Armenian_ZHE': 0x100053a, 'Armenian_zhe': 0x100056a, 'Armenian_INI': 0x100053b, 'Armenian_ini': 0x100056b, 'Armenian_LYUN': 0x100053c, 'Armenian_lyun': 0x100056c, 'Armenian_KHE': 0x100053d, 'Armenian_khe': 0x100056d, 'Armenian_TSA': 0x100053e, 'Armenian_tsa': 0x100056e, 'Armenian_KEN': 0x100053f, 'Armenian_ken': 0x100056f, 'Armenian_HO': 0x1000540, 'Armenian_ho': 0x1000570, 'Armenian_DZA': 0x1000541, 'Armenian_dza': 0x1000571, 'Armenian_GHAT': 0x1000542, 'Armenian_ghat': 0x1000572, 'Armenian_TCHE': 0x1000543, 'Armenian_tche': 0x1000573, 'Armenian_MEN': 0x1000544, 'Armenian_men': 0x1000574, 'Armenian_HI': 0x1000545, 'Armenian_hi': 0x1000575, 'Armenian_NU': 0x1000546, 'Armenian_nu': 0x1000576, 'Armenian_SHA': 0x1000547, 'Armenian_sha': 0x1000577, 'Armenian_VO': 0x1000548, 'Armenian_vo': 0x1000578, 'Armenian_CHA': 0x1000549, 'Armenian_cha': 0x1000579, 'Armenian_PE': 0x100054a, 'Armenian_pe': 0x100057a, 'Armenian_JE': 0x100054b, 'Armenian_je': 0x100057b, 'Armenian_RA': 0x100054c, 'Armenian_ra': 0x100057c, 'Armenian_SE': 0x100054d, 'Armenian_se': 0x100057d, 'Armenian_VEV': 0x100054e, 'Armenian_vev': 0x100057e, 'Armenian_TYUN': 0x100054f, 'Armenian_tyun': 0x100057f, 'Armenian_RE': 0x1000550, 'Armenian_re': 0x1000580, 'Armenian_TSO': 0x1000551, 'Armenian_tso': 0x1000581, 'Armenian_VYUN': 0x1000552, 'Armenian_vyun': 0x1000582, 'Armenian_PYUR': 0x1000553, 'Armenian_pyur': 0x1000583, 'Armenian_KE': 0x1000554, 'Armenian_ke': 0x1000584, 'Armenian_O': 0x1000555, 'Armenian_o': 0x1000585, 'Armenian_FE': 0x1000556, 'Armenian_fe': 0x1000586, 'Armenian_apostrophe': 0x100055a, 'Georgian_an': 0x10010d0, 'Georgian_ban': 0x10010d1, 'Georgian_gan': 0x10010d2, 'Georgian_don': 0x10010d3, 'Georgian_en': 0x10010d4, 'Georgian_vin': 0x10010d5, 'Georgian_zen': 0x10010d6, 'Georgian_tan': 0x10010d7, 'Georgian_in': 0x10010d8, 'Georgian_kan': 0x10010d9, 'Georgian_las': 0x10010da, 'Georgian_man': 0x10010db, 'Georgian_nar': 0x10010dc, 'Georgian_on': 0x10010dd, 'Georgian_par': 0x10010de, 'Georgian_zhar': 0x10010df, 'Georgian_rae': 0x10010e0, 'Georgian_san': 0x10010e1, 'Georgian_tar': 0x10010e2, 'Georgian_un': 0x10010e3, 'Georgian_phar': 0x10010e4, 'Georgian_khar': 0x10010e5, 'Georgian_ghan': 0x10010e6, 'Georgian_qar': 0x10010e7, 'Georgian_shin': 0x10010e8, 'Georgian_chin': 0x10010e9, 'Georgian_can': 0x10010ea, 'Georgian_jil': 0x10010eb, 'Georgian_cil': 0x10010ec, 'Georgian_char': 0x10010ed, 'Georgian_xan': 0x10010ee, 'Georgian_jhan': 0x10010ef, 'Georgian_hae': 0x10010f0, 'Georgian_he': 0x10010f1, 'Georgian_hie': 0x10010f2, 'Georgian_we': 0x10010f3, 'Georgian_har': 0x10010f4, 'Georgian_hoe': 0x10010f5, 'Georgian_fi': 0x10010f6, 'Xabovedot': 0x1001e8a, 'Ibreve': 0x100012c, 'Zstroke': 0x10001b5, 'Gcaron': 0x10001e6, 'Ocaron': 0x10001d1, 'Obarred': 0x100019f, 'xabovedot': 0x1001e8b, 'ibreve': 0x100012d, 'zstroke': 0x10001b6, 'gcaron': 0x10001e7, 'ocaron': 0x10001d2, 'obarred': 0x1000275, 'SCHWA': 0x100018f, 'schwa': 0x1000259, 'Lbelowdot': 0x1001e36, 'lbelowdot': 0x1001e37, 'Abelowdot': 0x1001ea0, 'abelowdot': 0x1001ea1, 'Ahook': 0x1001ea2, 'ahook': 0x1001ea3, 'Acircumflexacute': 0x1001ea4, 'acircumflexacute': 0x1001ea5, 'Acircumflexgrave': 0x1001ea6, 'acircumflexgrave': 0x1001ea7, 'Acircumflexhook': 0x1001ea8, 'acircumflexhook': 0x1001ea9, 'Acircumflextilde': 0x1001eaa, 'acircumflextilde': 0x1001eab, 'Acircumflexbelowdot': 0x1001eac, 'acircumflexbelowdot': 0x1001ead, 'Abreveacute': 0x1001eae, 'abreveacute': 0x1001eaf, 'Abrevegrave': 0x1001eb0, 'abrevegrave': 0x1001eb1, 'Abrevehook': 0x1001eb2, 'abrevehook': 0x1001eb3, 'Abrevetilde': 0x1001eb4, 'abrevetilde': 0x1001eb5, 'Abrevebelowdot': 0x1001eb6, 'abrevebelowdot': 0x1001eb7, 'Ebelowdot': 0x1001eb8, 'ebelowdot': 0x1001eb9, 'Ehook': 0x1001eba, 'ehook': 0x1001ebb, 'Etilde': 0x1001ebc, 'etilde': 0x1001ebd, 'Ecircumflexacute': 0x1001ebe, 'ecircumflexacute': 0x1001ebf, 'Ecircumflexgrave': 0x1001ec0, 'ecircumflexgrave': 0x1001ec1, 'Ecircumflexhook': 0x1001ec2, 'ecircumflexhook': 0x1001ec3, 'Ecircumflextilde': 0x1001ec4, 'ecircumflextilde': 0x1001ec5, 'Ecircumflexbelowdot': 0x1001ec6, 'ecircumflexbelowdot': 0x1001ec7, 'Ihook': 0x1001ec8, 'ihook': 0x1001ec9, 'Ibelowdot': 0x1001eca, 'ibelowdot': 0x1001ecb, 'Obelowdot': 0x1001ecc, 'obelowdot': 0x1001ecd, 'Ohook': 0x1001ece, 'ohook': 0x1001ecf, 'Ocircumflexacute': 0x1001ed0, 'ocircumflexacute': 0x1001ed1, 'Ocircumflexgrave': 0x1001ed2, 'ocircumflexgrave': 0x1001ed3, 'Ocircumflexhook': 0x1001ed4, 'ocircumflexhook': 0x1001ed5, 'Ocircumflextilde': 0x1001ed6, 'ocircumflextilde': 0x1001ed7, 'Ocircumflexbelowdot': 0x1001ed8, 'ocircumflexbelowdot': 0x1001ed9, 'Ohornacute': 0x1001eda, 'ohornacute': 0x1001edb, 'Ohorngrave': 0x1001edc, 'ohorngrave': 0x1001edd, 'Ohornhook': 0x1001ede, 'ohornhook': 0x1001edf, 'Ohorntilde': 0x1001ee0, 'ohorntilde': 0x1001ee1, 'Ohornbelowdot': 0x1001ee2, 'ohornbelowdot': 0x1001ee3, 'Ubelowdot': 0x1001ee4, 'ubelowdot': 0x1001ee5, 'Uhook': 0x1001ee6, 'uhook': 0x1001ee7, 'Uhornacute': 0x1001ee8, 'uhornacute': 0x1001ee9, 'Uhorngrave': 0x1001eea, 'uhorngrave': 0x1001eeb, 'Uhornhook': 0x1001eec, 'uhornhook': 0x1001eed, 'Uhorntilde': 0x1001eee, 'uhorntilde': 0x1001eef, 'Uhornbelowdot': 0x1001ef0, 'uhornbelowdot': 0x1001ef1, 'Ybelowdot': 0x1001ef4, 'ybelowdot': 0x1001ef5, 'Yhook': 0x1001ef6, 'yhook': 0x1001ef7, 'Ytilde': 0x1001ef8, 'ytilde': 0x1001ef9, 'Ohorn': 0x10001a0, 'ohorn': 0x10001a1, 'Uhorn': 0x10001af, 'uhorn': 0x10001b0, 'EcuSign': 0x10020a0, 'ColonSign': 0x10020a1, 'CruzeiroSign': 0x10020a2, 'FFrancSign': 0x10020a3, 'LiraSign': 0x10020a4, 'MillSign': 0x10020a5, 'NairaSign': 0x10020a6, 'PesetaSign': 0x10020a7, 'RupeeSign': 0x10020a8, 'WonSign': 0x10020a9, 'NewSheqelSign': 0x10020aa, 'DongSign': 0x10020ab, 'EuroSign': 0x20ac, 'zerosuperior': 0x1002070, 'foursuperior': 0x1002074, 'fivesuperior': 0x1002075, 'sixsuperior': 0x1002076, 'sevensuperior': 0x1002077, 'eightsuperior': 0x1002078, 'ninesuperior': 0x1002079, 'zerosubscript': 0x1002080, 'onesubscript': 0x1002081, 'twosubscript': 0x1002082, 'threesubscript': 0x1002083, 'foursubscript': 0x1002084, 'fivesubscript': 0x1002085, 'sixsubscript': 0x1002086, 'sevensubscript': 0x1002087, 'eightsubscript': 0x1002088, 'ninesubscript': 0x1002089, 'partdifferential': 0x1002202, 'emptyset': 0x1002205, 'elementof': 0x1002208, 'notelementof': 0x1002209, 'containsas': 0x100220B, 'squareroot': 0x100221A, 'cuberoot': 0x100221B, 'fourthroot': 0x100221C, 'dintegral': 0x100222C, 'tintegral': 0x100222D, 'because': 0x1002235, 'approxeq': 0x1002248, 'notapproxeq': 0x1002247, 'notidentical': 0x1002262, 'stricteq': 0x1002263, 'braille_dot_1': 0xfff1, 'braille_dot_2': 0xfff2, 'braille_dot_3': 0xfff3, 'braille_dot_4': 0xfff4, 'braille_dot_5': 0xfff5, 'braille_dot_6': 0xfff6, 'braille_dot_7': 0xfff7, 'braille_dot_8': 0xfff8, 'braille_dot_9': 0xfff9, 'braille_dot_10': 0xfffa, 'braille_blank': 0x1002800, 'braille_dots_1': 0x1002801, 'braille_dots_2': 0x1002802, 'braille_dots_12': 0x1002803, 'braille_dots_3': 0x1002804, 'braille_dots_13': 0x1002805, 'braille_dots_23': 0x1002806, 'braille_dots_123': 0x1002807, 'braille_dots_4': 0x1002808, 'braille_dots_14': 0x1002809, 'braille_dots_24': 0x100280a, 'braille_dots_124': 0x100280b, 'braille_dots_34': 0x100280c, 'braille_dots_134': 0x100280d, 'braille_dots_234': 0x100280e, 'braille_dots_1234': 0x100280f, 'braille_dots_5': 0x1002810, 'braille_dots_15': 0x1002811, 'braille_dots_25': 0x1002812, 'braille_dots_125': 0x1002813, 'braille_dots_35': 0x1002814, 'braille_dots_135': 0x1002815, 'braille_dots_235': 0x1002816, 'braille_dots_1235': 0x1002817, 'braille_dots_45': 0x1002818, 'braille_dots_145': 0x1002819, 'braille_dots_245': 0x100281a, 'braille_dots_1245': 0x100281b, 'braille_dots_345': 0x100281c, 'braille_dots_1345': 0x100281d, 'braille_dots_2345': 0x100281e, 'braille_dots_12345': 0x100281f, 'braille_dots_6': 0x1002820, 'braille_dots_16': 0x1002821, 'braille_dots_26': 0x1002822, 'braille_dots_126': 0x1002823, 'braille_dots_36': 0x1002824, 'braille_dots_136': 0x1002825, 'braille_dots_236': 0x1002826, 'braille_dots_1236': 0x1002827, 'braille_dots_46': 0x1002828, 'braille_dots_146': 0x1002829, 'braille_dots_246': 0x100282a, 'braille_dots_1246': 0x100282b, 'braille_dots_346': 0x100282c, 'braille_dots_1346': 0x100282d, 'braille_dots_2346': 0x100282e, 'braille_dots_12346': 0x100282f, 'braille_dots_56': 0x1002830, 'braille_dots_156': 0x1002831, 'braille_dots_256': 0x1002832, 'braille_dots_1256': 0x1002833, 'braille_dots_356': 0x1002834, 'braille_dots_1356': 0x1002835, 'braille_dots_2356': 0x1002836, 'braille_dots_12356': 0x1002837, 'braille_dots_456': 0x1002838, 'braille_dots_1456': 0x1002839, 'braille_dots_2456': 0x100283a, 'braille_dots_12456': 0x100283b, 'braille_dots_3456': 0x100283c, 'braille_dots_13456': 0x100283d, 'braille_dots_23456': 0x100283e, 'braille_dots_123456': 0x100283f, 'braille_dots_7': 0x1002840, 'braille_dots_17': 0x1002841, 'braille_dots_27': 0x1002842, 'braille_dots_127': 0x1002843, 'braille_dots_37': 0x1002844, 'braille_dots_137': 0x1002845, 'braille_dots_237': 0x1002846, 'braille_dots_1237': 0x1002847, 'braille_dots_47': 0x1002848, 'braille_dots_147': 0x1002849, 'braille_dots_247': 0x100284a, 'braille_dots_1247': 0x100284b, 'braille_dots_347': 0x100284c, 'braille_dots_1347': 0x100284d, 'braille_dots_2347': 0x100284e, 'braille_dots_12347': 0x100284f, 'braille_dots_57': 0x1002850, 'braille_dots_157': 0x1002851, 'braille_dots_257': 0x1002852, 'braille_dots_1257': 0x1002853, 'braille_dots_357': 0x1002854, 'braille_dots_1357': 0x1002855, 'braille_dots_2357': 0x1002856, 'braille_dots_12357': 0x1002857, 'braille_dots_457': 0x1002858, 'braille_dots_1457': 0x1002859, 'braille_dots_2457': 0x100285a, 'braille_dots_12457': 0x100285b, 'braille_dots_3457': 0x100285c, 'braille_dots_13457': 0x100285d, 'braille_dots_23457': 0x100285e, 'braille_dots_123457': 0x100285f, 'braille_dots_67': 0x1002860, 'braille_dots_167': 0x1002861, 'braille_dots_267': 0x1002862, 'braille_dots_1267': 0x1002863, 'braille_dots_367': 0x1002864, 'braille_dots_1367': 0x1002865, 'braille_dots_2367': 0x1002866, 'braille_dots_12367': 0x1002867, 'braille_dots_467': 0x1002868, 'braille_dots_1467': 0x1002869, 'braille_dots_2467': 0x100286a, 'braille_dots_12467': 0x100286b, 'braille_dots_3467': 0x100286c, 'braille_dots_13467': 0x100286d, 'braille_dots_23467': 0x100286e, 'braille_dots_123467': 0x100286f, 'braille_dots_567': 0x1002870, 'braille_dots_1567': 0x1002871, 'braille_dots_2567': 0x1002872, 'braille_dots_12567': 0x1002873, 'braille_dots_3567': 0x1002874, 'braille_dots_13567': 0x1002875, 'braille_dots_23567': 0x1002876, 'braille_dots_123567': 0x1002877, 'braille_dots_4567': 0x1002878, 'braille_dots_14567': 0x1002879, 'braille_dots_24567': 0x100287a, 'braille_dots_124567': 0x100287b, 'braille_dots_34567': 0x100287c, 'braille_dots_134567': 0x100287d, 'braille_dots_234567': 0x100287e, 'braille_dots_1234567': 0x100287f, 'braille_dots_8': 0x1002880, 'braille_dots_18': 0x1002881, 'braille_dots_28': 0x1002882, 'braille_dots_128': 0x1002883, 'braille_dots_38': 0x1002884, 'braille_dots_138': 0x1002885, 'braille_dots_238': 0x1002886, 'braille_dots_1238': 0x1002887, 'braille_dots_48': 0x1002888, 'braille_dots_148': 0x1002889, 'braille_dots_248': 0x100288a, 'braille_dots_1248': 0x100288b, 'braille_dots_348': 0x100288c, 'braille_dots_1348': 0x100288d, 'braille_dots_2348': 0x100288e, 'braille_dots_12348': 0x100288f, 'braille_dots_58': 0x1002890, 'braille_dots_158': 0x1002891, 'braille_dots_258': 0x1002892, 'braille_dots_1258': 0x1002893, 'braille_dots_358': 0x1002894, 'braille_dots_1358': 0x1002895, 'braille_dots_2358': 0x1002896, 'braille_dots_12358': 0x1002897, 'braille_dots_458': 0x1002898, 'braille_dots_1458': 0x1002899, 'braille_dots_2458': 0x100289a, 'braille_dots_12458': 0x100289b, 'braille_dots_3458': 0x100289c, 'braille_dots_13458': 0x100289d, 'braille_dots_23458': 0x100289e, 'braille_dots_123458': 0x100289f, 'braille_dots_68': 0x10028a0, 'braille_dots_168': 0x10028a1, 'braille_dots_268': 0x10028a2, 'braille_dots_1268': 0x10028a3, 'braille_dots_368': 0x10028a4, 'braille_dots_1368': 0x10028a5, 'braille_dots_2368': 0x10028a6, 'braille_dots_12368': 0x10028a7, 'braille_dots_468': 0x10028a8, 'braille_dots_1468': 0x10028a9, 'braille_dots_2468': 0x10028aa, 'braille_dots_12468': 0x10028ab, 'braille_dots_3468': 0x10028ac, 'braille_dots_13468': 0x10028ad, 'braille_dots_23468': 0x10028ae, 'braille_dots_123468': 0x10028af, 'braille_dots_568': 0x10028b0, 'braille_dots_1568': 0x10028b1, 'braille_dots_2568': 0x10028b2, 'braille_dots_12568': 0x10028b3, 'braille_dots_3568': 0x10028b4, 'braille_dots_13568': 0x10028b5, 'braille_dots_23568': 0x10028b6, 'braille_dots_123568': 0x10028b7, 'braille_dots_4568': 0x10028b8, 'braille_dots_14568': 0x10028b9, 'braille_dots_24568': 0x10028ba, 'braille_dots_124568': 0x10028bb, 'braille_dots_34568': 0x10028bc, 'braille_dots_134568': 0x10028bd, 'braille_dots_234568': 0x10028be, 'braille_dots_1234568': 0x10028bf, 'braille_dots_78': 0x10028c0, 'braille_dots_178': 0x10028c1, 'braille_dots_278': 0x10028c2, 'braille_dots_1278': 0x10028c3, 'braille_dots_378': 0x10028c4, 'braille_dots_1378': 0x10028c5, 'braille_dots_2378': 0x10028c6, 'braille_dots_12378': 0x10028c7, 'braille_dots_478': 0x10028c8, 'braille_dots_1478': 0x10028c9, 'braille_dots_2478': 0x10028ca, 'braille_dots_12478': 0x10028cb, 'braille_dots_3478': 0x10028cc, 'braille_dots_13478': 0x10028cd, 'braille_dots_23478': 0x10028ce, 'braille_dots_123478': 0x10028cf, 'braille_dots_578': 0x10028d0, 'braille_dots_1578': 0x10028d1, 'braille_dots_2578': 0x10028d2, 'braille_dots_12578': 0x10028d3, 'braille_dots_3578': 0x10028d4, 'braille_dots_13578': 0x10028d5, 'braille_dots_23578': 0x10028d6, 'braille_dots_123578': 0x10028d7, 'braille_dots_4578': 0x10028d8, 'braille_dots_14578': 0x10028d9, 'braille_dots_24578': 0x10028da, 'braille_dots_124578': 0x10028db, 'braille_dots_34578': 0x10028dc, 'braille_dots_134578': 0x10028dd, 'braille_dots_234578': 0x10028de, 'braille_dots_1234578': 0x10028df, 'braille_dots_678': 0x10028e0, 'braille_dots_1678': 0x10028e1, 'braille_dots_2678': 0x10028e2, 'braille_dots_12678': 0x10028e3, 'braille_dots_3678': 0x10028e4, 'braille_dots_13678': 0x10028e5, 'braille_dots_23678': 0x10028e6, 'braille_dots_123678': 0x10028e7, 'braille_dots_4678': 0x10028e8, 'braille_dots_14678': 0x10028e9, 'braille_dots_24678': 0x10028ea, 'braille_dots_124678': 0x10028eb, 'braille_dots_34678': 0x10028ec, 'braille_dots_134678': 0x10028ed, 'braille_dots_234678': 0x10028ee, 'braille_dots_1234678': 0x10028ef, 'braille_dots_5678': 0x10028f0, 'braille_dots_15678': 0x10028f1, 'braille_dots_25678': 0x10028f2, 'braille_dots_125678': 0x10028f3, 'braille_dots_35678': 0x10028f4, 'braille_dots_135678': 0x10028f5, 'braille_dots_235678': 0x10028f6, 'braille_dots_1235678': 0x10028f7, 'braille_dots_45678': 0x10028f8, 'braille_dots_145678': 0x10028f9, 'braille_dots_245678': 0x10028fa, 'braille_dots_1245678': 0x10028fb, 'braille_dots_345678': 0x10028fc, 'braille_dots_1345678': 0x10028fd, 'braille_dots_2345678': 0x10028fe, 'braille_dots_12345678': 0x10028ff, } qtile-0.10.7/requirements-dev.txt000066400000000000000000000000341305063162100167470ustar00rootroot00000000000000flake8 pytest-cov pip-tools qtile-0.10.7/requirements.in000066400000000000000000000000321305063162100157600ustar00rootroot00000000000000cairocffi cffi six xcffib qtile-0.10.7/requirements.txt000066400000000000000000000003401305063162100161730ustar00rootroot00000000000000# # This file is autogenerated by pip-compile # To update, run: # # pip-compile --output-file requirements.txt requirements.in # cairocffi==0.8.0 cffi==1.9.1 pycparser==2.17 # via cffi six==1.10.0 xcffib==0.5.1 qtile-0.10.7/resources/000077500000000000000000000000001305063162100147245ustar00rootroot00000000000000qtile-0.10.7/resources/README000066400000000000000000000006611305063162100156070ustar00rootroot00000000000000objgraph.dot: The Qtile object graph, in graphviz dot format. Render like so: circo -Tpng objgraph.dot > objgraph.png qtile.desktop: A desktop file for qtile telling session managers about qtile and how to run it. This file should be installed to /usr/share/xsessions qtile.1: man page for qtile (generated by setup.py, see Makefile for details) qshell.1: man page for qsh (generated by setup.py, see Makefile for details) qtile-0.10.7/resources/objgraph.dot000066400000000000000000000010021305063162100172210ustar00rootroot00000000000000 digraph G { root = "root"; splines = true; root -> bar; root -> group; root -> layout; root -> screen; root -> widget; root -> window; bar -> screen; group -> layout; group -> screen; group -> window; layout -> group; layout -> screen; layout -> window; screen -> bar; screen -> layout; screen -> window; widget -> bar; widget -> group; widget -> screen; window -> group; window -> screen; window -> layout; } qtile-0.10.7/resources/qshell.1000066400000000000000000000030151305063162100162750ustar00rootroot00000000000000.\" Man page generated from reStructuredText. . .TH "QSHELL" "1" "Feb 14, 2017" "0.10.7" "Qtile" .SH NAME qshell \- Qtile Documentation . .nr rst2man-indent-level 0 . .de1 rstReportMargin \\$1 \\n[an-margin] level \\n[rst2man-indent-level] level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] - \\n[rst2man-indent0] \\n[rst2man-indent1] \\n[rst2man-indent2] .. .de1 INDENT .\" .rstReportMargin pre: . RS \\$1 . nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] . nr rst2man-indent-level +1 .\" .rstReportMargin post: .. .de UNINDENT . RE .\" indent \\n[an-margin] .\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] .nr rst2man-indent-level -1 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. .SH SYNOPSIS .sp qshell [\-s qtilesocket] .SH DESCRIPTION .sp \fBqshell\fP is the remote shell tool for \fBqtile\fP\&. \fBqshell\fP supports manipulating various pieces of qtile remotely via an interactive shell. .SH OPTIONS .INDENT 0.0 .INDENT 3.5 .INDENT 0.0 .TP .BI \-s \ qtilesocket\fP,\fB \ \-\-socket \ qtilesocket Use qtilesocket for IPC communication. This can be useful if you are trying to connect to/inspect another X session\(aqs running qtile instance from your own X session (or perhaps restart qtile from a tty). .UNINDENT .UNINDENT .UNINDENT .SH BUGS .sp Bugs can be reported to the issue tracker at \fI\%http://github.com/qtile/qtile\fP\&. .SH AUTHOR Tycho Andersen .SH COPYRIGHT 2008-2016, Aldo Cortesi and contributers .\" Generated by docutils manpage writer. . qtile-0.10.7/resources/qtile.1000066400000000000000000000040101305063162100161170ustar00rootroot00000000000000.\" Man page generated from reStructuredText. . .TH "QTILE" "1" "Feb 14, 2017" "0.10.7" "Qtile" .SH NAME qtile \- Qtile Documentation . .nr rst2man-indent-level 0 . .de1 rstReportMargin \\$1 \\n[an-margin] level \\n[rst2man-indent-level] level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] - \\n[rst2man-indent0] \\n[rst2man-indent1] \\n[rst2man-indent2] .. .de1 INDENT .\" .rstReportMargin pre: . RS \\$1 . nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] . nr rst2man-indent-level +1 .\" .rstReportMargin post: .. .de UNINDENT . RE .\" indent \\n[an-margin] .\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] .nr rst2man-indent-level -1 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. .SH SYNOPSIS .sp qtile [\-c config] [\-s qtilesocket] [\-l DEBUG] [\-n] .SH DESCRIPTION .sp \fBqtile\fP runs Qtile on the current $DISPLAY. Qtile is a tiling window manager written in python. Complete configuration information is available online at \fI\%http://docs.qtile.org\fP\&. .SH OPTIONS .INDENT 0.0 .INDENT 3.5 .INDENT 0.0 .TP .BI \-c \ config\fP,\fB \ \-\-config \ config Use the specified config file. .TP .BI \-s \ qtilesocket\fP,\fB \ \-\-socket \ qtilesocket Use qtilesocket as the IPC server. .TP .BI \-l \ DEBUG\fP,\fB \ \-\-log\-level \ DEBUG Set the default log level, one of DEBUG, INFO, WARNING, ERROR, CRITICAL. .UNINDENT .UNINDENT .UNINDENT .SH FILES .sp Qtile searches for configuration files in the following locations: .INDENT 0.0 .INDENT 3.5 .INDENT 0.0 .IP 1. 3 The location specified by the \fB\-c\fP option. .IP 2. 3 \fB$XDG_CONFIG_HOME/qtile/config.py\fP .IP 3. 3 \fB~/.config/qtile/config.py\fP .IP 4. 3 The default configuration, distributed as the python module \fBlibqtile.resources.default_config\fP\&. .UNINDENT .UNINDENT .UNINDENT .SH BUGS .sp Bugs can be reported to the issue tracker at \fI\%http://github.com/qtile/qtile\fP\&. .SH AUTHOR Tycho Andersen .SH COPYRIGHT 2008-2016, Aldo Cortesi and contributers .\" Generated by docutils manpage writer. . qtile-0.10.7/resources/qtile.desktop000066400000000000000000000001401305063162100174300ustar00rootroot00000000000000[Desktop Entry] Name=Qtile Comment=Qtile Session Exec=qtile Type=Application Keywords=wm;tiling qtile-0.10.7/rpm/000077500000000000000000000000001305063162100135105ustar00rootroot00000000000000qtile-0.10.7/rpm/qtile.spec000066400000000000000000000127751305063162100155160ustar00rootroot00000000000000Summary: A pure-Python tiling window manager Name: qtile Version: 0.10.7 Release: 1%{?dist} Source0: https://github.com/qtile/qtile/archive/v%{version}.tar.gz License: MIT and GPLv3+ # All MIT except for: # libqtile/widget/pacman.py:GPL (v3 or later) BuildArch: noarch Url: http://qtile.org Source1: qtile.desktop BuildRequires: python3-devel BuildRequires: python3-setuptools BuildRequires: python3-cffi BuildRequires: python3-nose-cov BuildRequires: python3-xcffib BuildRequires: python3-trollius BuildRequires: python3-cairocffi BuildRequires: cairo BuildRequires: python3-six BuildRequires: python3-pycparser Requires: python3-cairocffi Requires: python3-cffi Requires: python3-xcffib Requires: python3-trollius # python3-cairocffi is not currently pulling in cairo Requires: cairo %description A pure-Python tiling window manager. Features ======== * Simple, small and extensible. It's easy to write your own layouts, widgets and commands. * Configured in Python. * Command shell that allows all aspects of Qtile to be managed and inspected. * Complete remote scriptability - write scripts to set up workspaces, manipulate windows, update status bar widgets and more. * Qtile's remote scriptability makes it one of the most thoroughly unit-tested window mangers around. %prep %setup -q -n qtile-%{version} %build %{__python3} setup.py build %install %{__python3} setup.py install --single-version-externally-managed -O1 --root=%{buildroot} --record=INSTALLED_FILES mkdir -p %{buildroot}%{_datadir}/xsessions/ install -m 644 %{SOURCE1} %{buildroot}%{_datadir}/xsessions/ %files %license LICENSE %doc README.rst %{_mandir}/man1/qshell.1* %{_mandir}/man1/qtile.1* %{_bindir}/qshell %{_bindir}/iqshell %{_bindir}/qtile %{_bindir}/qtile-run %{_bindir}/qtile-top %{python3_sitelib}/qtile-%{version}-py%{python3_version}.egg-info %{python3_sitelib}/libqtile %{_datadir}/xsessions/qtile.desktop %changelog * Tue Feb 14 2017 John Dulaney - 0.10.7-1 - new MPD widget, widget.MPD2, based on `mpd2` library - add option to ignore duplicates in prompt widget - add additional margin options to GroupBox widget - add option to ignore mouse wheel to GroupBox widget - add `watts` formatting string option to Battery widgets - add volume commands to Volume widget - add Window.focus command * Sat Feb 11 2017 Fedora Release Engineering - 0.10.6-4 - Rebuilt for https://fedoraproject.org/wiki/Fedora_26_Mass_Rebuild * Mon Dec 19 2016 Miro Hrončok - 0.10.6-3 - Rebuild for Python 3.6 * Tue Jul 19 2016 Fedora Release Engineering - 0.10.6-2 - https://fedoraproject.org/wiki/Changes/Automatic_Provides_for_Python_RPM_Packages * Wed May 25 2016 John Dulaney - 0.10.6-1 - Add `startup_complete` hook - Restore dynamic groups on restart - Major bug fixes with floating window handling * Fri Mar 04 2016 John Dulaney - 0.10.5-1 - Python 3.2 support dropped !!! - GoogleCalendar widget dropped for KhalCalendar widget !!! - qtile-session script removed in favor of qtile script !!! - new Columns layout, composed of dynamic and configurable columns of windows - new iPython kernel for qsh, called iqsh, see docs for installing - new qsh command `display_kb` to show current key binding - add json interface to IPC server - add commands for resizing MonadTall main panel - wlan widget shows when you are disconnected and uses a configurable format - fix path handling in PromptWidget - fix KeyboardLayout widget cycling keyboard - properly guard against setting screen to too large screen index * Thu Feb 04 2016 Fedora Release Engineering - 0.10.4-3 - Rebuilt for https://fedoraproject.org/wiki/Fedora_24_Mass_Rebuild * Wed Jan 20 2016 John Dulaney - 0.10.4-2 - Fix rpmlint issues * Tue Jan 19 2016 John Dulaney - 0.10.4-1 - New release * Fri Dec 25 2015 John Dulaney - 0.10.3-1 - New upstream release * Fri Nov 20 2015 John Dulaney - 0.10.2-5 - Build against new python-xcffib * Tue Nov 10 2015 Fedora Release Engineering - 0.10.2-4 - Rebuilt for https://fedoraproject.org/wiki/Changes/python3.5 * Wed Oct 21 2015 John Dulaney - 0.10.2-3 - Fix minor issue with spec file. * Tue Oct 20 2015 John Dulaney - 0.10.2-2 - /usr/bin/qtile-top to files list * Tue Oct 20 2015 John Dulaney - 0.10.2-1 - Update to latest upstream * Mon Oct 19 2015 John Dulaney - 0.10.1-1 - Fix soname issue * Mon Aug 03 2015 John Dulaney - 0.10.1-0 - Update to latest upstream * Mon Aug 03 2015 John Dulaney - 0.9.1-4 - Use Python3 * Thu Jun 18 2015 Fedora Release Engineering - 0.9.1-3 - Rebuilt for https://fedoraproject.org/wiki/Fedora_23_Mass_Rebuild * Sun Feb 22 2015 John Dulaney - 0.9.1-2 - Final update to licensing * Sat Feb 14 2015 John Dulaney - 0.9.1-1 - Update for new upstream release - Fix license headers. * Sun Feb 01 2015 John Dulaney - 0.9.0-2 - Update spec for qtile-0.9.0 - Include in Fedora. * Wed Oct 08 2014 John Dulaney - 0.8.0-1 - Initial packaging - Spec based on python-nose qtile-0.10.7/scripts/000077500000000000000000000000001305063162100144015ustar00rootroot00000000000000qtile-0.10.7/scripts/addlicence.sh000077500000000000000000000047021305063162100170160ustar00rootroot00000000000000#!/bin/bash # Copyright (c) 2008, Aldo Cortesi. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. set -e set -x # path to: http://0pointer.de/public/copyright.py COPYRIGHT=~/packages/copyright.py LICENSE="# # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the \"Software\"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. " files=$(licensecheck -r . | grep -v 'bin/libqtile' | grep UNKNOWN | cut -d: -f1) for f in $files; do { echo "$($COPYRIGHT "$f")"; echo "$LICENSE"; cat "$f"; } > newfile && mv newfile "$f" done qtile-0.10.7/scripts/genkeysyms000077500000000000000000000011751305063162100165310ustar00rootroot00000000000000#!/usr/bin/env python """ This script is used to generate xkeysyms.py from the X keysymdef.h definition header file. """ import os, os.path, sys import re template = '''\ keysyms = { %s }\ ''' def genkeysyms(path): XRE = r"#define\s+XK_(\S+)\s+(\S+)" ks = [] for i in file(path): m = re.match(XRE, i) if m: ks.append(m.groups()) lines = [" '%s': %s," % i for i in ks] print(template % '\n'.join(lines)) if __name__ == "__main__": if len(sys.argv) != 2: sys.stderr.write("Usage: genkeysyms path/to/keysymdef.h\n") sys.exit(1) genkeysyms(sys.argv[1]) qtile-0.10.7/scripts/qtb000077500000000000000000000026771305063162100151310ustar00rootroot00000000000000#!/usr/bin/env python """ Command-line interaction with Qtile TextBox widgets. """ from __future__ import print_function import os, sys from argparse import ArgumentParser from libqtile import command def main(): parser = ArgumentParser() parser.add_argument('--version', action='version', version='%(prog)s 0.2') parser.add_argument( "-l", "--list", action="store_true", help="List addressable widgets" ) parser.add_argument( "-s", "--socket", type=str, default=None, help='Use specified communication socket' ) # in order for the -l option to work, we can't require these args, so we # check for them later parser.add_argument( "name", type=str, required=False, help='Name of widget to change' ) parser.add_argument( "text", type=str, required=False, help='Text to to place in widget' ) args = parser.parse_args() client = command.Client(args.socket) if args.list: for i in client.list_widgets(): print(i) else: if args.name is None or args.text is None: parser.error("Please specify widget name and argument.") try: client.widget.__getitem__(args.name).update(args.text) except command.CommandError as v: print(v, file=sys.stderr) sys.exit(1) if __name__ == "__main__": main() qtile-0.10.7/scripts/xephyr000077500000000000000000000006541305063162100156530ustar00rootroot00000000000000#!/bin/sh HERE=$(dirname $(readlink -f $0)) SCREEN_SIZE=${SCREEN_SIZE:-800x600} XDISPLAY=:1 LOG_LEVEL=${LOG_LEVEL:-INFO} if [[ -z PYTHON ]]; then PYTHON=python fi Xephyr +extension RANDR -screen ${SCREEN_SIZE} ${XDISPLAY} -ac & XEPHYR_PID=$! ( sleep 1 env DISPLAY=${XDISPLAY} ${PYTHON} "${HERE}"/../bin/qtile -l ${LOG_LEVEL} $@ & QTILE_PID=$! env DISPLAY=${XDISPLAY} xterm & wait $QTILE_PID kill $XEPHYR_PID ) qtile-0.10.7/setup.cfg000066400000000000000000000005651305063162100145410ustar00rootroot00000000000000# E301: expected 1 blank line, found 0 # E302: number of blank lines # E501: line too long # E128: continuation line under-indented for visual indent # E305: expected 2 blank lines after class or function definition, found 1 [flake8] ignore = E301,E302,E501,E128,E305 exclude = libqtile/_ffi*.py [tool:pytest] python_files = test_*.py testpaths = test addopts = --verbose qtile-0.10.7/setup.py000077500000000000000000000131051305063162100144270ustar00rootroot00000000000000#!/usr/bin/env python # Copyright (c) 2008 Aldo Cortesi # Copyright (c) 2011 Mounier Florian # Copyright (c) 2012 dmpayton # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 roger # Copyright (c) 2014 Pedro Algarvio # Copyright (c) 2014-2015 Tycho Andersen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import os import sys import textwrap from setuptools import setup from setuptools.command.install import install class CheckCairoXcb(install): def cairo_xcb_check(self): try: from cairocffi import cairo cairo.cairo_xcb_surface_create return True except AttributeError: return False def finalize_options(self): if not self.cairo_xcb_check(): print(textwrap.dedent(""" It looks like your cairocffi was not built with xcffib support. To fix this: - Ensure a recent xcffib is installed (pip install 'xcffib>=0.5.0') - The pip cache is cleared (remove ~/.cache/pip, if it exists) - Reinstall cairocffi, either: pip install --no-deps --ignore-installed cairocffi or pip uninstall cairocffi && pip install cairocffi """)) sys.exit(1) install.finalize_options(self) long_description = """ A pure-Python tiling window manager. Features ======== * Simple, small and extensible. It's easy to write your own layouts, widgets and commands. * Configured in Python. * Command shell that allows all aspects of Qtile to be managed and inspected. * Complete remote scriptability - write scripts to set up workspaces, manipulate windows, update status bar widgets and more. * Qtile's remote scriptability makes it one of the most thoroughly unit-tested window mangers around. """ if '_cffi_backend' in sys.builtin_module_names: import _cffi_backend requires_cffi = "cffi==" + _cffi_backend.__version__ else: requires_cffi = "cffi>=1.1.0" # PyPy < 2.6 compatibility if requires_cffi.startswith("cffi==0."): cffi_args = dict( zip_safe=False ) else: cffi_args = dict(cffi_modules=[ 'libqtile/ffi_build.py:pango_ffi', 'libqtile/ffi_build.py:xcursors_ffi' ]) dependencies = ['xcffib>=0.5.0', 'cairocffi>=0.7', 'six>=1.4.1', requires_cffi] if sys.version_info >= (3, 4): pass elif sys.version_info >= (3, 3): dependencies.append('asyncio') else: dependencies.append('trollius') setup( name="qtile", version="0.10.7", description="A pure-Python tiling window manager.", long_description=long_description, classifiers=[ "Intended Audience :: End Users/Desktop", "License :: OSI Approved :: MIT License", "Development Status :: 3 - Alpha", "Programming Language :: Python", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Operating System :: Unix", "Topic :: Desktop Environment :: Window Managers", ], keywords="qtile tiling window manager", author="Aldo Cortesi", author_email="aldo@nullcube.com", maintainer="Sean Vig", maintainer_email="sean.v.775@gmail.com", url="http://qtile.org", license="MIT", install_requires=dependencies, setup_requires=dependencies, extras_require={ 'ipython': ["ipykernel", "jupyter_console"], }, packages=['libqtile', 'libqtile.interactive', 'libqtile.layout', 'libqtile.scripts', 'libqtile.widget', 'libqtile.extention', 'libqtile.resources' ], package_data={'libqtile.resources': [ '{}/*'.format(item) for item in os.listdir('libqtile/resources') if os.path.isdir(os.path.join('libqtile/resources', item)) ]}, entry_points={ 'console_scripts': [ 'qtile = libqtile.scripts.qtile:main', 'qtile-run = libqtile.scripts.qtile_run:main', 'qtile-top = libqtile.scripts.qtile_top:main', 'qshell = libqtile.scripts.qshell:main', ] }, scripts=[ 'bin/iqshell', ], data_files=[ ('share/man/man1', ['resources/qtile.1', 'resources/qshell.1'])], cmdclass={'install': CheckCairoXcb}, **cffi_args ) qtile-0.10.7/test/000077500000000000000000000000001305063162100136715ustar00rootroot00000000000000qtile-0.10.7/test/__init__.py000066400000000000000000000000001305063162100157700ustar00rootroot00000000000000qtile-0.10.7/test/configs/000077500000000000000000000000001305063162100153215ustar00rootroot00000000000000qtile-0.10.7/test/configs/basic.py000066400000000000000000000023501305063162100167540ustar00rootroot00000000000000# Copyright (c) 2011 Florian Mounier # Copyright (c) 2012 Tycho Andersen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from libqtile import config keys = [ config.Key(["control"], "k", "focusnext") ] screens = [] layouts = [] groups = [] qtile-0.10.7/test/configs/syntaxerr.py000066400000000000000000000022631305063162100177350ustar00rootroot00000000000000# Copyright (c) 2008 Aldo Cortesi # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # # It is really dumb that we need a giant copyright notice for this one line # file. Fucking lawyers :( sdf [ qtile-0.10.7/test/conftest.py000066400000000000000000000321571305063162100161000ustar00rootroot00000000000000# Copyright (c) 2011 Florian Mounier # Copyright (c) 2011 Anshuman Bhaduri # Copyright (c) 2014 Sean Vig # Copyright (c) 2014-2015 Tycho Andersen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import print_function import libqtile import libqtile.ipc from libqtile.manager import Qtile as QtileManager from libqtile.log_utils import init_log import logging import multiprocessing import os import pytest import subprocess import sys import tempfile import time import traceback import xcffib import xcffib.xproto # the default sizes for the Xephyr windows WIDTH = 800 HEIGHT = 600 SECOND_WIDTH = 640 SECOND_HEIGHT = 480 max_sleep = 10 sleep_time = 0.05 def _find_display(): """Returns the next available display""" display = 1 while os.path.exists("/tmp/.X{0}-lock".format(display)): display += 1 return display def whereis(program): """Search PATH for executable""" for path in os.environ.get('PATH', '').split(':'): if os.path.exists(os.path.join(path, program)) and \ not os.path.isdir(os.path.join(path, program)): return os.path.join(path, program) return None class BareConfig(object): auto_fullscreen = True groups = [ libqtile.config.Group("a"), libqtile.config.Group("b"), libqtile.config.Group("c"), libqtile.config.Group("d") ] layouts = [ libqtile.layout.stack.Stack(num_stacks=1), libqtile.layout.stack.Stack(num_stacks=2) ] floating_layout = libqtile.layout.floating.Floating() keys = [ libqtile.config.Key( ["control"], "k", libqtile.command._Call([("layout", None)], "up") ), libqtile.config.Key( ["control"], "j", libqtile.command._Call([("layout", None)], "down") ), ] mouse = [] screens = [libqtile.config.Screen()] main = None follow_mouse_focus = False class Xephyr(object): """Spawn Xephyr instance Set-up a Xephyr instance with the given parameters. The Xephyr instance must be started, and then stopped. """ def __init__(self, xinerama=True, randr=False, two_screens=True, width=WIDTH, height=HEIGHT, xoffset=None): self.xinerama, self.randr = xinerama, randr self.two_screens = two_screens self.width = width self.height = height if xoffset is None: self.xoffset = width else: self.xoffset = xoffset self.proc = None # Handle to Xephyr instance, subprocess.Popen object self.display = None def start_xephyr(self): """Start Xephyr instance Starts the Xephyr instance and sets the `self.display` to the display which is used to setup the instance. """ # we'll try twice to open Xephyr for _ in range(2): # get a new display self.display = ":{}".format(_find_display()) # build up arguments args = [ "Xephyr", "-name", "qtile_test", self.display, "-ac", "-screen", "%sx%s" % (self.width, self.height)] if self.two_screens: args.extend(["-origin", "%s,0" % self.xoffset, "-screen", "%sx%s" % (SECOND_WIDTH, SECOND_HEIGHT)]) if self.xinerama: args.extend(["+xinerama"]) if self.randr: args.extend(["+extension", "RANDR"]) self.proc = subprocess.Popen(args) start = time.time() # wait for X display to come up while self.proc.poll() is None and time.time() < start + max_sleep: try: conn = xcffib.connect(self.display) except xcffib.ConnectionException: time.sleep(sleep_time) else: conn.disconnect() return else: # we wern't able to get a display up self.display = None raise AssertionError("Unable to start Xephyr, quit with return code {:d}".format( self.proc.returncode )) def stop_xephyr(self): """Stop the Xephyr instance""" # Xephyr must be started first if self.proc is None: return # Kill xephyr only if it is running if self.proc.poll() is None: # We should always be able to kill xephyr nicely self.proc.terminate() self.proc.wait() self.proc = None class Qtile(object): """Spawn a Qtile instance Setup a qtile server instance on the given display, with the given socket and log files. The qtile server must be started, and then stopped when it is done. Windows can be spawned for the qtile instance to interact with with various `.test_*` methods. """ def __init__(self, sockfile, display): self.sockfile = sockfile self.display = display self.proc = None self.c = None self.testwindows = [] def start(self, config_class): rpipe, wpipe = multiprocessing.Pipe() def run_qtile(): try: init_log(logging.INFO, log_path=None) q = QtileManager(config_class(), self.display, self.sockfile) q.loop() except Exception: wpipe.send(traceback.format_exc()) self.proc = multiprocessing.Process(target=run_qtile) self.proc.start() # First, wait for socket to appear start = time.time() while time.time() < start + max_sleep: if os.path.exists(self.sockfile): break if rpipe.poll(sleep_time): error = rpipe.recv() raise AssertionError("Error launching Qtile, traceback:\n%s" % error) else: raise AssertionError("Error launching Qtile, socket never came up") self.c = libqtile.command.Client(self.sockfile) # Next, wait for server to come up start = time.time() while time.time() < start + max_sleep: try: if self.c.status() == "OK": break except libqtile.ipc.IPCError: pass if rpipe.poll(sleep_time): error = rpipe.recv() raise AssertionError("Error launching Qtile, traceback:\n%s" % error) else: raise AssertionError("Error launching Qtile, quit without exception") def create_manager(self, config_class): """Create a Qtile manager instance in this thread This should only be used when it is known that the manager will throw an error and the returned manager should not be started, otherwise this will likely block the thread. """ init_log(logging.INFO, log_path=None) return QtileManager(config_class(), self.display, self.sockfile) def terminate(self): if self.proc is None: print("Qtile is not alive", file=sys.stderr) else: # try to send SIGTERM and wait up to 10 sec to quit self.proc.terminate() self.proc.join(10) if self.proc.is_alive(): # desperate times... this probably messes with multiprocessing... try: os.kill(self.proc.pid, 9) self.proc.join() except OSError: # The process may have died due to some other error pass if self.proc.exitcode: print("Qtile exited with exitcode: %d" % self.proc.exitcode, file=sys.stderr) self.proc = None for proc in self.testwindows[:]: proc.terminate() proc.wait() self.testwindows.remove(proc) def _spawn_window(self, *args): """Starts a program which opens a window Spawns a new subprocess for a command that opens a window, given by the arguments to this method. Spawns the new process and checks that qtile maps the new window. """ if not args: raise AssertionError("Trying to run nothing! (missing arguments)") start = len(self.c.windows()) proc = subprocess.Popen(args, env={"DISPLAY": self.display}) while proc.poll() is None: try: if len(self.c.windows()) > start: break except RuntimeError: pass time.sleep(sleep_time) else: proc.terminate() proc.wait() raise AssertionError("Window never appeared...") self.testwindows.append(proc) return proc def kill_window(self, proc): """Kill a window and check that qtile unmaps it Kills a window created by calling one of the `self.test*` methods, ensuring that qtile removes it from the `windows` attribute. """ assert proc in self.testwindows, "Given process is not a spawned window" start = len(self.c.windows()) proc.terminate() proc.wait() self.testwindows.remove(proc) for _ in range(100): if len(self.c.windows()) < start: break time.sleep(sleep_time) else: raise AssertionError("Window could not be killed...") def testWindow(self, name): python = sys.executable d = os.path.dirname(os.path.realpath(__file__)) python = sys.executable path = os.path.join(d, "scripts", "window.py") return self._spawn_window(python, path, self.display, name) def testXclock(self): path = whereis("xclock") return self._spawn_window(path) def testXeyes(self): path = whereis("xeyes") return self._spawn_window(path) def testGkrellm(self): path = whereis("gkrellm") return self._spawn_window(path) def testXterm(self): path = whereis("xterm") return self._spawn_window(path) def groupconsistency(self): groups = self.c.groups() screens = self.c.screens() seen = set() for g in groups.values(): scrn = g["screen"] if scrn is not None: if scrn in seen: raise AssertionError( "Screen referenced from more than one group.") seen.add(scrn) assert screens[scrn]["group"] == g["name"] assert len(seen) == len(screens), "Not all screens \ had an attached group." @pytest.yield_fixture(scope="session") def xvfb(): display = ":{:d}".format(_find_display()) args = ["Xvfb", display, "-screen", "0", "800x600x16"] proc = subprocess.Popen(args) try: # wait for X display to come up start = time.time() while proc.poll() is None and time.time() < start + max_sleep: try: conn = xcffib.connect(display) except xcffib.ConnectionException: time.sleep(sleep_time) else: conn.disconnect() break else: raise OSError("Xvfb did not come up") os.environ["DISPLAY"] = display yield finally: proc.terminate() proc.wait() @pytest.yield_fixture(scope="function") def xephyr(request, xvfb): kwargs = getattr(request, "param", {}) x = Xephyr(**kwargs) try: x.start_xephyr() yield x finally: x.stop_xephyr() @pytest.yield_fixture(scope="function") def qtile(request, xephyr): config = getattr(request, "param", BareConfig) with tempfile.NamedTemporaryFile() as f: sockfile = f.name q = Qtile(sockfile, xephyr.display) try: q.start(config) yield q finally: q.terminate() @pytest.yield_fixture(scope="function") def qtile_nospawn(request, xephyr): with tempfile.NamedTemporaryFile() as f: sockfile = f.name q = Qtile(sockfile, xephyr.display) try: yield q finally: q.terminate() no_xinerama = pytest.mark.parametrize("xephyr", [{"xinerama": False}], indirect=True) qtile-0.10.7/test/layouts/000077500000000000000000000000001305063162100153715ustar00rootroot00000000000000qtile-0.10.7/test/layouts/__init__.py000066400000000000000000000000001305063162100174700ustar00rootroot00000000000000qtile-0.10.7/test/layouts/layout_utils.py000066400000000000000000000045031305063162100205020ustar00rootroot00000000000000# Copyright (c) 2011 Florian Mounier # Copyright (c) 2012, 2014-2015 Tycho Andersen # Copyright (c) 2013 Mattias Svala # Copyright (c) 2013 Craig Barnes # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Adi Sieker # Copyright (c) 2014 Chris Wesseling # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. def assertFocused(self, name): """Asserts that window with specified name is currently focused""" info = self.c.window.info() assert info['name'] == name, 'Got {0!r}, expected {1!r}'.format( info['name'], name) def assertDimensions(self, x, y, w, h, win=None): """Asserts dimensions of window""" if win is None: win = self.c.window info = win.info() assert info['x'] == x, info assert info['y'] == y, info assert info['width'] == w, info # why? assert info['height'] == h, info def assertFocusPath(self, *names): for i in names: self.c.group.next_window() assertFocused(self, i) # let's check twice for sure for i in names: self.c.group.next_window() assertFocused(self, i) # Ok, let's check backwards now for i in reversed(names): assertFocused(self, i) self.c.group.prev_window() # and twice for sure for i in reversed(names): assertFocused(self, i) self.c.group.prev_window() qtile-0.10.7/test/layouts/test_floating.py000066400000000000000000000046261305063162100206150ustar00rootroot00000000000000# Copyright (c) 2008, Aldo Cortesi. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest from libqtile import layout import libqtile.manager import libqtile.config from .layout_utils import assertFocused from ..conftest import no_xinerama class FloatingConfig(object): auto_fullscreen = True main = None groups = [ libqtile.config.Group("a"), ] layouts = [ layout.Floating() ] floating_layout = libqtile.layout.floating.Floating() keys = [] mouse = [] screens = [] follow_mouse_focus = False floating_config = lambda x: \ no_xinerama(pytest.mark.parametrize("qtile", [FloatingConfig], indirect=True)(x)) @floating_config def test_float_next_prev_window(qtile): self = qtile # spawn three windows self.testWindow("one") self.testWindow("two") self.testWindow("three") # focus previous windows assertFocused(self, "three") self.c.group.prev_window() assertFocused(self, "two") self.c.group.prev_window() assertFocused(self, "one") # checking that it loops around properly self.c.group.prev_window() assertFocused(self, "three") # focus next windows # checking that it loops around properly self.c.group.next_window() assertFocused(self, "one") self.c.group.next_window() assertFocused(self, "two") self.c.group.next_window() assertFocused(self, "three") qtile-0.10.7/test/layouts/test_matrix.py000066400000000000000000000070421305063162100203110ustar00rootroot00000000000000# Copyright (c) 2011 Florian Mounier # Copyright (c) 2012, 2014-2015 Tycho Andersen # Copyright (c) 2013 Mattias Svala # Copyright (c) 2013 Craig Barnes # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Adi Sieker # Copyright (c) 2014 Chris Wesseling # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest from libqtile import layout import libqtile.manager import libqtile.config from ..conftest import no_xinerama class MatrixConfig(object): auto_fullscreen = True main = None groups = [ libqtile.config.Group("a"), libqtile.config.Group("b"), libqtile.config.Group("c"), libqtile.config.Group("d") ] layouts = [ layout.Matrix(columns=2) ] floating_layout = libqtile.layout.floating.Floating() keys = [] mouse = [] screens = [] matrix_config = lambda x: \ no_xinerama(pytest.mark.parametrize("qtile", [MatrixConfig], indirect=True)(x)) @matrix_config def test_matrix_simple(qtile): qtile.testWindow("one") assert qtile.c.layout.info()["rows"] == [["one"]] qtile.testWindow("two") assert qtile.c.layout.info()["rows"] == [["one", "two"]] qtile.testWindow("three") assert qtile.c.layout.info()["rows"] == [["one", "two"], ["three"]] @matrix_config def test_matrix_navigation(qtile): qtile.testWindow("one") qtile.testWindow("two") qtile.testWindow("three") qtile.testWindow("four") qtile.testWindow("five") qtile.c.layout.right() assert qtile.c.layout.info()["current_window"] == (0, 2) qtile.c.layout.up() assert qtile.c.layout.info()["current_window"] == (0, 1) qtile.c.layout.up() assert qtile.c.layout.info()["current_window"] == (0, 0) qtile.c.layout.up() assert qtile.c.layout.info()["current_window"] == (0, 2) qtile.c.layout.down() assert qtile.c.layout.info()["current_window"] == (0, 0) qtile.c.layout.down() assert qtile.c.layout.info()["current_window"] == (0, 1) qtile.c.layout.right() assert qtile.c.layout.info()["current_window"] == (1, 1) qtile.c.layout.right() assert qtile.c.layout.info()["current_window"] == (0, 1) @matrix_config def test_matrix_add_remove_columns(qtile): qtile.testWindow("one") qtile.testWindow("two") qtile.testWindow("three") qtile.testWindow("four") qtile.testWindow("five") qtile.c.layout.add() assert qtile.c.layout.info()["rows"] == [["one", "two", "three"], ["four", "five"]] qtile.c.layout.delete() assert qtile.c.layout.info()["rows"] == [["one", "two"], ["three", "four"], ["five"]] qtile-0.10.7/test/layouts/test_max.py000066400000000000000000000160341305063162100175730ustar00rootroot00000000000000# Copyright (c) 2011 Florian Mounier # Copyright (c) 2012, 2014-2015 Tycho Andersen # Copyright (c) 2013 Mattias Svala # Copyright (c) 2013 Craig Barnes # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Adi Sieker # Copyright (c) 2014 Chris Wesseling # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest from libqtile import layout import libqtile.manager import libqtile.config from ..conftest import no_xinerama class MaxConfig(object): auto_fullscreen = True main = None groups = [ libqtile.config.Group("a"), libqtile.config.Group("b"), libqtile.config.Group("c"), libqtile.config.Group("d") ] layouts = [ layout.Max() ] floating_layout = libqtile.layout.floating.Floating() keys = [] mouse = [] screens = [] max_config = lambda x: \ no_xinerama(pytest.mark.parametrize("qtile", [MaxConfig], indirect=True)(x)) @max_config def test_max_simple(qtile): qtile.testWindow("one") assert qtile.c.layout.info()["clients"] == ["one"] qtile.testWindow("two") assert qtile.c.layout.info()["clients"] == ["one", "two"] @max_config def test_max_updown(qtile): qtile.testWindow("one") qtile.testWindow("two") qtile.testWindow("three") assert qtile.c.layout.info()["clients"] == ["one", "two", "three"] qtile.c.layout.up() assert qtile.c.groups()["a"]["focus"] == "two" qtile.c.layout.down() assert qtile.c.groups()["a"]["focus"] == "three" @max_config def test_max_remove(qtile): qtile.testWindow("one") two = qtile.testWindow("two") assert qtile.c.layout.info()["clients"] == ["one", "two"] qtile.kill_window(two) assert qtile.c.layout.info()["clients"] == ["one"] @max_config def test_closing_dialog(qtile): # Closing a floating window that has focus must return the focus to the # window that was previously focused # Start by testing a dialog that is the first open window in the group dialog1 = qtile.testWindow("dialog1") qtile.testWindow("one") qtile.testWindow("two") three = qtile.testWindow("three") qtile.c.layout.down() assert qtile.c.window.info()['name'] == "dialog1", qtile.c.window.info()['name'] qtile.c.window.toggle_floating() qtile.kill_window(dialog1) assert qtile.c.window.info()['name'] == "three", qtile.c.window.info()['name'] # Now test a dialog that is the last open window in the group dialog2 = qtile.testWindow("dialog2") qtile.c.window.toggle_floating() qtile.kill_window(dialog2) assert qtile.c.window.info()['name'] == "three", qtile.c.window.info()['name'] # Now test a dialog that is neither the first nor the last open window in # the group dialog3 = qtile.testWindow("dialog3") four = qtile.testWindow("four") qtile.testWindow("five") qtile.testWindow("six") # TODO: for a more generic test, find a way to focus 'five', then focus # 'dialog3' skipping 'four', so that then, after closing 'dialog3', the # focus must be returned to 'five', which better represents a generic # window that wasn't necessarily opened immediately after the dialog qtile.c.layout.up() qtile.c.layout.up() qtile.c.layout.up() assert qtile.c.window.info()['name'] == "dialog3", qtile.c.window.info()['name'] qtile.c.window.toggle_floating() qtile.kill_window(dialog3) assert qtile.c.window.info()['name'] == "four", qtile.c.window.info()['name'] # Finally test a case in which the window that had focus previously is # closed without stealing focus from the dialog, thus requiring to find the # window that had focus even before that (this tests the history of focus) dialog4 = qtile.testWindow("dialog4") qtile.c.layout.up() qtile.c.layout.up() qtile.c.layout.up() assert qtile.c.window.info()['name'] == "two", qtile.c.window.info()['name'] qtile.c.layout.down() qtile.c.layout.down() qtile.c.layout.down() assert qtile.c.window.info()['name'] == "dialog4", qtile.c.window.info()['name'] qtile.c.window.toggle_floating() qtile.kill_window(three) qtile.kill_window(four) qtile.kill_window(dialog4) assert qtile.c.window.info()['name'] == "two", qtile.c.window.info()['name'] @max_config def test_closing_notification(qtile): # Closing a floating window that doesn't have focus must not change the # currently focused window # TODO: for more proper testing, the notification windows should be created # without giving them focus # Start by testing a notification that is the first open window in the # group notification1 = qtile.testWindow("notification1") qtile.c.window.toggle_floating() qtile.testWindow("one") qtile.testWindow("two") qtile.testWindow("three") qtile.c.layout.up() assert qtile.c.window.info()['name'] == "two", qtile.c.window.info()['name'] qtile.kill_window(notification1) assert qtile.c.window.info()['name'] == "two", qtile.c.window.info()['name'] # Now test a notification that is the last open window in the group qtile.c.layout.down() notification2 = qtile.testWindow("notification2") qtile.c.window.toggle_floating() # Create and kill 'temp', otherwise qtile.c.layout.up() won't work temp = qtile.testWindow("temp") qtile.c.layout.up() qtile.c.layout.up() qtile.kill_window(temp) assert qtile.c.window.info()['name'] == "two", qtile.c.window.info()['name'] qtile.kill_window(notification2) assert qtile.c.window.info()['name'] == "two", qtile.c.window.info()['name'] # Now test a notification that is neither the first nor the last open # window in the group qtile.c.layout.down() notification3 = qtile.testWindow("notification3") qtile.c.window.toggle_floating() four = qtile.testWindow("four") five = qtile.testWindow("five") qtile.c.layout.up() qtile.c.layout.up() qtile.c.layout.up() assert qtile.c.window.info()['name'] == "two", qtile.c.window.info()['name'] qtile.kill_window(notification3) assert qtile.c.window.info()['name'] == "two", qtile.c.window.info()['name'] qtile-0.10.7/test/layouts/test_ratiotile.py000066400000000000000000000200351305063162100207760ustar00rootroot00000000000000# Copyright (c) 2011 Florian Mounier # Copyright (c) 2012, 2014-2015 Tycho Andersen # Copyright (c) 2013 Mattias Svala # Copyright (c) 2013 Craig Barnes # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Adi Sieker # Copyright (c) 2014 Chris Wesseling # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest from time import sleep from libqtile import layout import libqtile.manager import libqtile.config from ..conftest import no_xinerama class RatioTileConfig(object): auto_fullscreen = True main = None groups = [ libqtile.config.Group("a"), libqtile.config.Group("b"), libqtile.config.Group("c"), libqtile.config.Group("d") ] layouts = [ layout.RatioTile(ratio=.5), layout.RatioTile(), ] floating_layout = libqtile.layout.floating.Floating() keys = [] mouse = [] screens = [] follow_mouse_focus = False ratiotile_config = lambda x: \ no_xinerama(pytest.mark.parametrize("qtile", [RatioTileConfig], indirect=True)(x)) @ratiotile_config def test_ratiotile_add_windows(qtile): for i in range(12): qtile.testWindow(str(i)) if i == 0: assert qtile.c.layout.info()['layout_info'] == [ (0, 0, 800, 600)] elif i == 1: assert qtile.c.layout.info()['layout_info'] == [ (0, 0, 400, 600), (400, 0, 400, 600)] elif i == 2: assert qtile.c.layout.info()['layout_info'] == [ (0, 0, 266, 600), (266, 0, 266, 600), (532, 0, 268, 600)] elif i == 3: assert qtile.c.layout.info()['layout_info'] == [ (0, 0, 200, 600), (200, 0, 200, 600), (400, 0, 200, 600), (600, 0, 200, 600)] elif i == 4: assert qtile.c.layout.info()['layout_info'] == [ (0, 0, 160, 600), (160, 0, 160, 600), (320, 0, 160, 600), (480, 0, 160, 600), (640, 0, 160, 600)] elif i == 5: assert qtile.c.layout.info()['layout_info'] == [ (0, 0, 133, 600), (133, 0, 133, 600), (266, 0, 133, 600), (399, 0, 133, 600), (532, 0, 133, 600), (665, 0, 135, 600)] elif i == 6: assert qtile.c.layout.info()['layout_info'] == [ (0, 0, 200, 300), (200, 0, 200, 300), (400, 0, 200, 300), (600, 0, 200, 300), (0, 300, 266, 300), (266, 300, 266, 300), (532, 300, 268, 300)] elif i == 7: assert qtile.c.layout.info()['layout_info'] == [ (0, 0, 200, 300), (200, 0, 200, 300), (400, 0, 200, 300), (600, 0, 200, 300), (0, 300, 200, 300), (200, 300, 200, 300), (400, 300, 200, 300), (600, 300, 200, 300)] elif i == 8: assert qtile.c.layout.info()['layout_info'] == [ (0, 0, 160, 300), (160, 0, 160, 300), (320, 0, 160, 300), (480, 0, 160, 300), (640, 0, 160, 300), (0, 300, 200, 300), (200, 300, 200, 300), (400, 300, 200, 300), (600, 300, 200, 300)] elif i == 9: assert qtile.c.layout.info()['layout_info'] == [ (0, 0, 160, 300), (160, 0, 160, 300), (320, 0, 160, 300), (480, 0, 160, 300), (640, 0, 160, 300), (0, 300, 160, 300), (160, 300, 160, 300), (320, 300, 160, 300), (480, 300, 160, 300), (640, 300, 160, 300)] elif i == 10: assert qtile.c.layout.info()['layout_info'] == [ (0, 0, 133, 300), (133, 0, 133, 300), (266, 0, 133, 300), (399, 0, 133, 300), (532, 0, 133, 300), (665, 0, 135, 300), (0, 300, 160, 300), (160, 300, 160, 300), (320, 300, 160, 300), (480, 300, 160, 300), (640, 300, 160, 300)] elif i == 11: assert qtile.c.layout.info()['layout_info'] == [ (0, 0, 133, 300), (133, 0, 133, 300), (266, 0, 133, 300), (399, 0, 133, 300), (532, 0, 133, 300), (665, 0, 135, 300), (0, 300, 133, 300), (133, 300, 133, 300), (266, 300, 133, 300), (399, 300, 133, 300), (532, 300, 133, 300), (665, 300, 135, 300)] else: assert False @ratiotile_config def test_ratiotile_add_windows_golden_ratio(qtile): qtile.c.next_layout() for i in range(12): qtile.testWindow(str(i)) if i == 0: assert qtile.c.layout.info()['layout_info'] == [ (0, 0, 800, 600)] elif i == 4: # the rest test col order assert qtile.c.layout.info()['layout_info'] == [ (0, 0, 400, 200), (0, 200, 400, 200), (0, 400, 400, 200), (400, 0, 400, 300), (400, 300, 400, 300)] elif i == 5: assert qtile.c.layout.info()['layout_info'] == [ (0, 0, 400, 200), (0, 200, 400, 200), (0, 400, 400, 200), (400, 0, 400, 200), (400, 200, 400, 200), (400, 400, 400, 200)] elif i == 9: assert qtile.c.layout.info()['layout_info'] == [ (0, 0, 266, 150), (0, 150, 266, 150), (0, 300, 266, 150), (0, 450, 266, 150), (266, 0, 266, 150), (266, 150, 266, 150), (266, 300, 266, 150), (266, 450, 266, 150), (532, 0, 266, 300), (532, 300, 266, 300)] elif i == 10: assert qtile.c.layout.info()['layout_info'] == [ (0, 0, 266, 150), (0, 150, 266, 150), (0, 300, 266, 150), (0, 450, 266, 150), (266, 0, 266, 150), (266, 150, 266, 150), (266, 300, 266, 150), (266, 450, 266, 150), (532, 0, 266, 200), (532, 200, 266, 200), (532, 400, 266, 200)] elif i == 11: assert qtile.c.layout.info()['layout_info'] == [ (0, 0, 266, 150), (0, 150, 266, 150), (0, 300, 266, 150), (0, 450, 266, 150), (266, 0, 266, 150), (266, 150, 266, 150), (266, 300, 266, 150), (266, 450, 266, 150), (532, 0, 266, 150), (532, 150, 266, 150), (532, 300, 266, 150), (532, 450, 266, 150)] @ratiotile_config def test_ratiotile_basic(qtile): qtile.testWindow("one") qtile.testWindow("two") qtile.testWindow("three") sleep(0.1) assert qtile.c.window.info()['width'] == 264 assert qtile.c.window.info()['height'] == 598 assert qtile.c.window.info()['x'] == 0 assert qtile.c.window.info()['y'] == 0 assert qtile.c.window.info()['name'] == 'three' qtile.c.group.next_window() assert qtile.c.window.info()['width'] == 264 assert qtile.c.window.info()['height'] == 598 assert qtile.c.window.info()['x'] == 266 assert qtile.c.window.info()['y'] == 0 assert qtile.c.window.info()['name'] == 'two' qtile.c.group.next_window() assert qtile.c.window.info()['width'] == 266 assert qtile.c.window.info()['height'] == 598 assert qtile.c.window.info()['x'] == 532 assert qtile.c.window.info()['y'] == 0 assert qtile.c.window.info()['name'] == 'one' qtile-0.10.7/test/layouts/test_slice.py000066400000000000000000000111521305063162100201010ustar00rootroot00000000000000# Copyright (c) 2011 Florian Mounier # Copyright (c) 2012, 2014-2015 Tycho Andersen # Copyright (c) 2013 Mattias Svala # Copyright (c) 2013 Craig Barnes # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Adi Sieker # Copyright (c) 2014 Chris Wesseling # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest from libqtile import layout import libqtile.manager import libqtile.config from .layout_utils import assertDimensions, assertFocused, assertFocusPath from ..conftest import no_xinerama class SliceConfig(object): auto_fullscreen = True main = None groups = [ libqtile.config.Group("a"), ] layouts = [ layout.Slice(side='left', width=200, wname='slice', fallback=layout.Stack(num_stacks=1, border_width=0)), layout.Slice(side='right', width=200, wname='slice', fallback=layout.Stack(num_stacks=1, border_width=0)), layout.Slice(side='top', width=200, wname='slice', fallback=layout.Stack(num_stacks=1, border_width=0)), layout.Slice(side='bottom', width=200, wname='slice', fallback=layout.Stack(num_stacks=1, border_width=0)), ] floating_layout = libqtile.layout.floating.Floating() keys = [] mouse = [] screens = [] follow_mouse_focus = False slice_config = lambda x: \ no_xinerama(pytest.mark.parametrize("qtile", [SliceConfig], indirect=True)(x)) @slice_config def test_no_slice(qtile): qtile.testWindow('one') assertDimensions(qtile, 200, 0, 600, 600) qtile.testWindow('two') assertDimensions(qtile, 200, 0, 600, 600) @slice_config def test_slice_first(qtile): qtile.testWindow('slice') assertDimensions(qtile, 0, 0, 200, 600) qtile.testWindow('two') assertDimensions(qtile, 200, 0, 600, 600) @slice_config def test_slice_last(qtile): qtile.testWindow('one') assertDimensions(qtile, 200, 0, 600, 600) qtile.testWindow('slice') assertDimensions(qtile, 0, 0, 200, 600) @slice_config def test_slice_focus(qtile): one = qtile.testWindow('one') assertFocused(qtile, 'one') two = qtile.testWindow('two') assertFocused(qtile, 'two') slice = qtile.testWindow('slice') assertFocused(qtile, 'slice') assertFocusPath(qtile, 'slice') three = qtile.testWindow('three') assertFocusPath(qtile, 'slice', 'three') qtile.kill_window(two) assertFocusPath(qtile, 'slice', 'one') qtile.kill_window(slice) assertFocusPath(qtile, 'one') slice = qtile.testWindow('slice') assertFocusPath(qtile, 'one', 'slice') @slice_config def test_all_slices(qtile): qtile.testWindow('slice') # left assertDimensions(qtile, 0, 0, 200, 600) qtile.c.next_layout() # right assertDimensions(qtile, 600, 0, 200, 600) qtile.c.next_layout() # top assertDimensions(qtile, 0, 0, 800, 200) qtile.c.next_layout() # bottom assertDimensions(qtile, 0, 400, 800, 200) qtile.c.next_layout() # left again qtile.testWindow('one') assertDimensions(qtile, 200, 0, 600, 600) qtile.c.next_layout() # right assertDimensions(qtile, 0, 0, 600, 600) qtile.c.next_layout() # top assertDimensions(qtile, 0, 200, 800, 400) qtile.c.next_layout() # bottom assertDimensions(qtile, 0, 0, 800, 400) @slice_config def test_command_propagation(qtile): qtile.testWindow('slice') qtile.testWindow('one') qtile.testWindow('two') info = qtile.c.layout.info() assert info['name'] == 'slice', info['name'] org_height = qtile.c.window.info()['height'] qtile.c.layout.toggle_split() assert qtile.c.window.info()['height'] != org_height qtile-0.10.7/test/layouts/test_stack.py000066400000000000000000000163201305063162100201110ustar00rootroot00000000000000# Copyright (c) 2011 Florian Mounier # Copyright (c) 2012, 2014-2015 Tycho Andersen # Copyright (c) 2013 Mattias Svala # Copyright (c) 2013 Craig Barnes # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Adi Sieker # Copyright (c) 2014 Chris Wesseling # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest from libqtile import layout import libqtile.manager import libqtile.config from ..conftest import no_xinerama class StackConfig(object): auto_fullscreen = True main = None groups = [ libqtile.config.Group("a"), libqtile.config.Group("b"), libqtile.config.Group("c"), libqtile.config.Group("d") ] layouts = [ layout.Stack(num_stacks=2), layout.Stack(num_stacks=1), ] floating_layout = libqtile.layout.floating.Floating() keys = [] mouse = [] screens = [] follow_mouse_focus = False stack_config = lambda x: \ no_xinerama(pytest.mark.parametrize("qtile", [StackConfig], indirect=True)(x)) def _stacks(self): stacks = [] for i in self.c.layout.info()["stacks"]: windows = i["clients"] current = i["current"] stacks.append(windows[current:] + windows[:current]) return stacks @stack_config def test_stack_commands(qtile): assert qtile.c.layout.info()["current_stack"] == 0 qtile.testWindow("one") assert _stacks(qtile) == [["one"], []] assert qtile.c.layout.info()["current_stack"] == 0 qtile.testWindow("two") assert _stacks(qtile) == [["one"], ["two"]] assert qtile.c.layout.info()["current_stack"] == 1 qtile.testWindow("three") assert _stacks(qtile) == [["one"], ["three", "two"]] assert qtile.c.layout.info()["current_stack"] == 1 qtile.c.layout.delete() assert _stacks(qtile) == [["one", "three", "two"]] info = qtile.c.groups()["a"] assert info["focus"] == "one" qtile.c.layout.delete() assert len(_stacks(qtile)) == 1 qtile.c.layout.add() assert _stacks(qtile) == [["one", "three", "two"], []] qtile.c.layout.rotate() assert _stacks(qtile) == [[], ["one", "three", "two"]] @stack_config def test_stack_cmd_down(qtile): qtile.c.layout.down() @stack_config def test_stack_addremove(qtile): one = qtile.testWindow("one") qtile.c.layout.next() two = qtile.testWindow("two") three = qtile.testWindow("three") assert _stacks(qtile) == [['one'], ['three', 'two']] assert qtile.c.layout.info()["current_stack"] == 1 qtile.kill_window(three) assert qtile.c.layout.info()["current_stack"] == 1 qtile.kill_window(two) assert qtile.c.layout.info()["current_stack"] == 0 qtile.c.layout.next() two = qtile.testWindow("two") qtile.c.layout.next() assert qtile.c.layout.info()["current_stack"] == 0 qtile.kill_window(one) assert qtile.c.layout.info()["current_stack"] == 1 @stack_config def test_stack_rotation(qtile): qtile.c.layout.delete() qtile.testWindow("one") qtile.testWindow("two") qtile.testWindow("three") assert _stacks(qtile) == [["three", "two", "one"]] qtile.c.layout.down() assert _stacks(qtile) == [["one", "three", "two"]] qtile.c.layout.up() assert _stacks(qtile) == [["three", "two", "one"]] qtile.c.layout.down() qtile.c.layout.down() assert _stacks(qtile) == [["two", "one", "three"]] @stack_config def test_stack_nextprev(qtile): qtile.c.layout.add() one = qtile.testWindow("one") two = qtile.testWindow("two") three = qtile.testWindow("three") assert qtile.c.groups()["a"]["focus"] == "three" qtile.c.layout.next() assert qtile.c.groups()["a"]["focus"] == "one" qtile.c.layout.previous() assert qtile.c.groups()["a"]["focus"] == "three" qtile.c.layout.previous() assert qtile.c.groups()["a"]["focus"] == "two" qtile.c.layout.next() qtile.c.layout.next() qtile.c.layout.next() assert qtile.c.groups()["a"]["focus"] == "two" qtile.kill_window(three) qtile.c.layout.next() assert qtile.c.groups()["a"]["focus"] == "one" qtile.c.layout.previous() assert qtile.c.groups()["a"]["focus"] == "two" qtile.c.layout.next() qtile.kill_window(two) qtile.c.layout.next() assert qtile.c.groups()["a"]["focus"] == "one" qtile.kill_window(one) qtile.c.layout.next() assert qtile.c.groups()["a"]["focus"] is None qtile.c.layout.previous() assert qtile.c.groups()["a"]["focus"] is None @stack_config def test_stack_window_removal(qtile): qtile.c.layout.next() one = qtile.testWindow("one") two = qtile.testWindow("two") qtile.c.layout.down() qtile.kill_window(two) @stack_config def test_stack_split(qtile): one = qtile.testWindow("one") two = qtile.testWindow("two") three = qtile.testWindow("three") stacks = qtile.c.layout.info()["stacks"] assert not stacks[1]["split"] qtile.c.layout.toggle_split() stacks = qtile.c.layout.info()["stacks"] assert stacks[1]["split"] @stack_config def test_stack_shuffle(qtile): qtile.c.next_layout() one = qtile.testWindow("one") two = qtile.testWindow("two") three = qtile.testWindow("three") stack = qtile.c.layout.info()["stacks"][0] assert stack["clients"][stack["current"]] == "three" for i in range(5): qtile.c.layout.shuffle_up() stack = qtile.c.layout.info()["stacks"][0] assert stack["clients"][stack["current"]] == "three" for i in range(5): qtile.c.layout.shuffle_down() stack = qtile.c.layout.info()["stacks"][0] assert stack["clients"][stack["current"]] == "three" @stack_config def test_stack_client_to(qtile): one = qtile.testWindow("one") two = qtile.testWindow("two") assert qtile.c.layout.info()["stacks"][0]["clients"] == ["one"] qtile.c.layout.client_to_previous() assert qtile.c.layout.info()["stacks"][0]["clients"] == ["two", "one"] qtile.c.layout.client_to_previous() assert qtile.c.layout.info()["stacks"][0]["clients"] == ["one"] assert qtile.c.layout.info()["stacks"][1]["clients"] == ["two"] qtile.c.layout.client_to_next() assert qtile.c.layout.info()["stacks"][0]["clients"] == ["two", "one"] @stack_config def test_stack_info(qtile): one = qtile.testWindow("one") assert qtile.c.layout.info()["stacks"] qtile-0.10.7/test/layouts/test_tile.py000066400000000000000000000073501305063162100177440ustar00rootroot00000000000000# Copyright (c) 2011 Florian Mounier # Copyright (c) 2012, 2014-2015 Tycho Andersen # Copyright (c) 2013 Mattias Svala # Copyright (c) 2013 Craig Barnes # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Adi Sieker # Copyright (c) 2014 Chris Wesseling # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest from libqtile import layout import libqtile.manager import libqtile.config from ..conftest import no_xinerama class TileConfig(object): auto_fullscreen = True main = None groups = [ libqtile.config.Group("a"), libqtile.config.Group("b"), libqtile.config.Group("c"), libqtile.config.Group("d") ] layouts = [ layout.Tile(), layout.Tile(masterWindows=2) ] floating_layout = libqtile.layout.floating.Floating() keys = [] mouse = [] screens = [] follow_mouse_focus = False tile_config = lambda x: \ no_xinerama(pytest.mark.parametrize("qtile", [TileConfig], indirect=True)(x)) @tile_config def test_tile_updown(qtile): qtile.testWindow("one") qtile.testWindow("two") qtile.testWindow("three") assert qtile.c.layout.info()["clients"] == ["three", "two", "one"] qtile.c.layout.down() assert qtile.c.layout.info()["clients"] == ["two", "one", "three"] qtile.c.layout.up() assert qtile.c.layout.info()["clients"] == ["three", "two", "one"] @tile_config def test_tile_nextprev(qtile): qtile.testWindow("one") qtile.testWindow("two") qtile.testWindow("three") assert qtile.c.layout.info()["clients"] == ["three", "two", "one"] assert qtile.c.groups()["a"]["focus"] == "three" qtile.c.layout.next() assert qtile.c.groups()["a"]["focus"] == "two" qtile.c.layout.previous() assert qtile.c.groups()["a"]["focus"] == "three" qtile.c.layout.previous() assert qtile.c.groups()["a"]["focus"] == "one" qtile.c.layout.next() qtile.c.layout.next() qtile.c.layout.next() assert qtile.c.groups()["a"]["focus"] == "one" @tile_config def test_tile_master_and_slave(qtile): qtile.testWindow("one") qtile.testWindow("two") qtile.testWindow("three") assert qtile.c.layout.info()["master"] == ["three"] assert qtile.c.layout.info()["slave"] == ["two", "one"] qtile.c.next_layout() assert qtile.c.layout.info()["master"] == ["three", "two"] assert qtile.c.layout.info()["slave"] == ["one"] @tile_config def test_tile_remove(qtile): one = qtile.testWindow("one") qtile.testWindow("two") three = qtile.testWindow("three") assert qtile.c.layout.info()["master"] == ["three"] qtile.kill_window(one) assert qtile.c.layout.info()["master"] == ["three"] qtile.kill_window(three) assert qtile.c.layout.info()["master"] == ["two"] qtile-0.10.7/test/layouts/test_verticaltile.py000066400000000000000000000051441305063162100214750ustar00rootroot00000000000000# Copyright (c) 2011 Florian Mounier # Copyright (c) 2012, 2014-2015 Tycho Andersen # Copyright (c) 2013 Mattias Svala # Copyright (c) 2013 Craig Barnes # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Adi Sieker # Copyright (c) 2014 Chris Wesseling # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest from libqtile import layout import libqtile.manager import libqtile.config from .layout_utils import assertDimensions from ..conftest import no_xinerama class VerticalTileConfig(object): auto_fullscreen = True main = None groups = [ libqtile.config.Group("a"), libqtile.config.Group("b"), libqtile.config.Group("c"), libqtile.config.Group("d") ] layouts = [ layout.VerticalTile(columns=2) ] floating_layout = libqtile.layout.floating.Floating() keys = [] mouse = [] screens = [] verticaltile_config = lambda x: \ no_xinerama(pytest.mark.parametrize("qtile", [VerticalTileConfig], indirect=True)(x)) @verticaltile_config def test_verticaltile_simple(qtile): qtile.testWindow("one") assertDimensions(qtile, 0, 0, 800, 600) qtile.testWindow("two") assertDimensions(qtile, 0, 300, 798, 298) qtile.testWindow("three") assertDimensions(qtile, 0, 400, 798, 198) @verticaltile_config def test_verticaltile_maximize(qtile): qtile.testWindow("one") assertDimensions(qtile, 0, 0, 800, 600) qtile.testWindow("two") assertDimensions(qtile, 0, 300, 798, 298) # Maximize the bottom layout, taking 75% of space qtile.c.layout.maximize() assertDimensions(qtile, 0, 150, 798, 448) qtile-0.10.7/test/layouts/test_xmonad.py000066400000000000000000000220641305063162100202740ustar00rootroot00000000000000# Copyright (c) 2015 Sean Vig # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest from libqtile import layout import libqtile.manager import libqtile.config from .layout_utils import assertDimensions, assertFocused from ..conftest import no_xinerama class MonadTallConfig(object): auto_fullscreen = True main = None groups = [ libqtile.config.Group("a") ] layouts = [ layout.MonadTall() ] floating_layout = libqtile.layout.floating.Floating() keys = [] mouse = [] screens = [] follow_mouse_focus = False monadtall_config = lambda x: \ no_xinerama(pytest.mark.parametrize("qtile", [MonadTallConfig], indirect=True)(x)) class MonadTallMarginsConfig(object): auto_fullscreen = True main = None groups = [ libqtile.config.Group("a") ] layouts = [ layout.MonadTall(margin=4) ] floating_layout = libqtile.layout.floating.Floating() keys = [] mouse = [] screens = [] follow_mouse_focus = False monadtallmargins_config = lambda x: \ no_xinerama(pytest.mark.parametrize("qtile", [MonadTallMarginsConfig], indirect=True)(x)) @monadtall_config def test_add_clients(qtile): qtile.testWindow('one') qtile.testWindow('two') assert qtile.c.layout.info()["main"] == 'one' assert qtile.c.layout.info()["secondary"] == ['two'] assertFocused(qtile, 'two') qtile.testWindow('three') assert qtile.c.layout.info()["main"] == 'one' assert qtile.c.layout.info()["secondary"] == ['two', 'three'] assertFocused(qtile, 'three') qtile.c.layout.previous() assertFocused(qtile, 'two') qtile.testWindow('four') assert qtile.c.layout.info()["main"] == 'one' assert qtile.c.layout.info()["secondary"] == ['two', 'four', 'three'] assertFocused(qtile, 'four') @monadtallmargins_config def test_margins(qtile): qtile.testWindow('one') assertDimensions(qtile, 4, 4, 788, 588) qtile.testWindow('two') assertFocused(qtile, 'two') assertDimensions(qtile, 404, 4, 388, 588) qtile.c.layout.previous() assertFocused(qtile, 'one') assertDimensions(qtile, 4, 4, 392, 588) @monadtall_config def test_growmain_solosecondary(qtile): qtile.testWindow('one') assertDimensions(qtile, 0, 0, 796, 596) qtile.testWindow('two') qtile.c.layout.previous() assertFocused(qtile, 'one') assertDimensions(qtile, 0, 0, 396, 596) qtile.c.layout.grow() # Grows 5% of 800 = 40 pixels assertDimensions(qtile, 0, 0, 436, 596) qtile.c.layout.shrink() assertDimensions(qtile, 0, 0, 396, 596) # Max width is 75% of 800 = 600 pixels for _ in range(10): qtile.c.layout.grow() assertDimensions(qtile, 0, 0, 596, 596) # Min width is 25% of 800 = 200 pixels for _ in range(10): qtile.c.layout.shrink() assertDimensions(qtile, 0, 0, 196, 596) @monadtall_config def test_growmain_multiplesecondary(qtile): qtile.testWindow('one') assertDimensions(qtile, 0, 0, 796, 596) qtile.testWindow('two') qtile.testWindow('three') qtile.c.layout.previous() qtile.c.layout.previous() assertFocused(qtile, 'one') assertDimensions(qtile, 0, 0, 396, 596) qtile.c.layout.grow() # Grows 5% of 800 = 40 pixels assertDimensions(qtile, 0, 0, 436, 596) qtile.c.layout.shrink() assertDimensions(qtile, 0, 0, 396, 596) # Max width is 75% of 800 = 600 pixels for _ in range(10): qtile.c.layout.grow() assertDimensions(qtile, 0, 0, 596, 596) # Min width is 25% of 800 = 200 pixels for _ in range(10): qtile.c.layout.shrink() assertDimensions(qtile, 0, 0, 196, 596) @monadtall_config def test_growsecondary_solosecondary(qtile): qtile.testWindow('one') assertDimensions(qtile, 0, 0, 796, 596) qtile.testWindow('two') assertFocused(qtile, 'two') assertDimensions(qtile, 400, 0, 396, 596) qtile.c.layout.grow() # Grows 5% of 800 = 40 pixels assertDimensions(qtile, 360, 0, 436, 596) qtile.c.layout.shrink() assertDimensions(qtile, 400, 0, 396, 596) # Max width is 75% of 800 = 600 pixels for _ in range(10): qtile.c.layout.grow() assertDimensions(qtile, 200, 0, 596, 596) # Min width is 25% of 800 = 200 pixels for _ in range(10): qtile.c.layout.shrink() assertDimensions(qtile, 600, 0, 196, 596) @monadtall_config def test_growsecondary_multiplesecondary(qtile): qtile.testWindow('one') assertDimensions(qtile, 0, 0, 796, 596) qtile.testWindow('two') qtile.testWindow('three') qtile.c.layout.previous() assertFocused(qtile, 'two') assertDimensions(qtile, 400, 0, 396, 296) # Grow 20 pixels qtile.c.layout.grow() assertDimensions(qtile, 400, 0, 396, 316) qtile.c.layout.shrink() assertDimensions(qtile, 400, 0, 396, 296) # Min height of other is 85 pixels, leaving 515 for _ in range(20): qtile.c.layout.grow() assertDimensions(qtile, 400, 0, 396, 511) # Min height of qtile is 85 pixels for _ in range(40): qtile.c.layout.shrink() assertDimensions(qtile, 400, 0, 396, 85) @monadtall_config def test_flip(qtile): qtile.testWindow('one') qtile.testWindow('two') qtile.testWindow('three') # Check all the dimensions qtile.c.layout.next() assertFocused(qtile, 'one') assertDimensions(qtile, 0, 0, 396, 596) qtile.c.layout.next() assertFocused(qtile, 'two') assertDimensions(qtile, 400, 0, 396, 296) qtile.c.layout.next() assertFocused(qtile, 'three') assertDimensions(qtile, 400, 300, 396, 296) # Now flip it and do it again qtile.c.layout.flip() qtile.c.layout.next() assertFocused(qtile, 'one') assertDimensions(qtile, 400, 0, 396, 596) qtile.c.layout.next() assertFocused(qtile, 'two') assertDimensions(qtile, 0, 0, 396, 296) qtile.c.layout.next() assertFocused(qtile, 'three') assertDimensions(qtile, 0, 300, 396, 296) @monadtall_config def test_shuffle(qtile): qtile.testWindow('one') qtile.testWindow('two') qtile.testWindow('three') qtile.testWindow('four') assert qtile.c.layout.info()['main'] == 'one' assert qtile.c.layout.info()['secondary'] == ['two', 'three', 'four'] qtile.c.layout.shuffle_up() assert qtile.c.layout.info()['main'] == 'one' assert qtile.c.layout.info()['secondary'] == ['two', 'four', 'three'] qtile.c.layout.shuffle_up() assert qtile.c.layout.info()['main'] == 'one' assert qtile.c.layout.info()['secondary'] == ['four', 'two', 'three'] qtile.c.layout.shuffle_up() assert qtile.c.layout.info()['main'] == 'four' assert qtile.c.layout.info()['secondary'] == ['one', 'two', 'three'] @monadtall_config def test_swap(qtile): qtile.testWindow('one') qtile.testWindow('two') qtile.testWindow('three') qtile.testWindow('focused') assert qtile.c.layout.info()['main'] == 'one' assert qtile.c.layout.info()['secondary'] == ['two', 'three', 'focused'] # Swap a secondary left, left aligned qtile.c.layout.swap_left() assert qtile.c.layout.info()['main'] == 'focused' assert qtile.c.layout.info()['secondary'] == ['two', 'three', 'one'] # Swap a main right, left aligned qtile.c.layout.swap_right() assert qtile.c.layout.info()['main'] == 'two' assert qtile.c.layout.info()['secondary'] == ['focused', 'three', 'one'] # flip over qtile.c.layout.flip() qtile.c.layout.shuffle_down() assert qtile.c.layout.info()['main'] == 'two' assert qtile.c.layout.info()['secondary'] == ['three', 'focused', 'one'] # Swap secondary right, right aligned qtile.c.layout.swap_right() assert qtile.c.layout.info()['main'] == 'focused' assert qtile.c.layout.info()['secondary'] == ['three', 'two', 'one'] # Swap main left, right aligned qtile.c.layout.swap_left() assert qtile.c.layout.info()['main'] == 'three' assert qtile.c.layout.info()['secondary'] == ['focused', 'two', 'one'] # Do swap main qtile.c.layout.swap_main() assert qtile.c.layout.info()['main'] == 'focused' assert qtile.c.layout.info()['secondary'] == ['three', 'two', 'one'] qtile-0.10.7/test/layouts/test_zoomy.py000066400000000000000000000043741305063162100201670ustar00rootroot00000000000000# Copyright (c) 2011 Florian Mounier # Copyright (c) 2012, 2014-2015 Tycho Andersen # Copyright (c) 2013 Mattias Svala # Copyright (c) 2013 Craig Barnes # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Adi Sieker # Copyright (c) 2014 Chris Wesseling # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest from libqtile import layout import libqtile.manager import libqtile.config from .layout_utils import assertDimensions, assertFocusPath from ..conftest import no_xinerama class ZoomyConfig(object): auto_fullscreen = True main = None groups = [ libqtile.config.Group("a"), ] layouts = [ layout.Zoomy(columnwidth=200), ] floating_layout = libqtile.layout.floating.Floating() keys = [] mouse = [] screens = [] zoomy_config = lambda x: \ no_xinerama(pytest.mark.parametrize("qtile", [ZoomyConfig], indirect=True)(x)) @zoomy_config def test_zoomy_one(qtile): qtile.testWindow('one') assertDimensions(qtile, 0, 0, 600, 600) qtile.testWindow('two') assertDimensions(qtile, 0, 0, 600, 600) qtile.testWindow('three') assertDimensions(qtile, 0, 0, 600, 600) assertFocusPath(qtile, 'two', 'one', 'three') # TODO(pc) find a way to check size of inactive windows qtile-0.10.7/test/scripts/000077500000000000000000000000001305063162100153605ustar00rootroot00000000000000qtile-0.10.7/test/scripts/window.py000066400000000000000000000100701305063162100172370ustar00rootroot00000000000000#!/usr/bin/env python # Copyright (c) 2011 Florian Mounier # Copyright (c) 2014 Sean Vig # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """ This program is carefully crafted to exercise a number of corner-cases in Qtile. """ from __future__ import print_function import sys import time import xcffib import xcffib.xproto def configure(window): window.configure( width=100, height=100, x=0, y=0, border_width=1, ) for i in range(20): try: conn = xcffib.connect(display=sys.argv[1]) except xcffib.ConnectionException: time.sleep(0.1) continue except Exception as v: print("Error opening test window: ", type(v), v, file=sys.stderr) sys.exit(1) break else: print("Could not open window on display %s" % (sys.argv[1]), file=sys.stderr) sys.exit(1) screen = conn.get_setup().roots[conn.pref_screen] window = conn.generate_id() background = conn.core.AllocColor(screen.default_colormap, 0x2828, 0x8383, 0xCECE).reply().pixel # Color "#2883ce" conn.core.CreateWindow(xcffib.CopyFromParent, window, screen.root, 100, 100, 100, 100, 1, xcffib.xproto.WindowClass.InputOutput, screen.root_visual, xcffib.xproto.CW.BackPixel | xcffib.xproto.CW.EventMask, [background, xcffib.xproto.EventMask.StructureNotify | xcffib.xproto.EventMask.Exposure]) conn.core.ChangeProperty(xcffib.xproto.PropMode.Replace, window, xcffib.xproto.Atom.WM_NAME, xcffib.xproto.Atom.STRING, 8, len(sys.argv[2]), sys.argv[2]) wm_protocols = "WM_PROTOCOLS" wm_protocols = conn.core.InternAtom(0, len(wm_protocols), wm_protocols).reply().atom wm_delete_window = "WM_DELETE_WINDOW" wm_delete_window = conn.core.InternAtom(0, len(wm_delete_window), wm_delete_window).reply().atom conn.core.ChangeProperty(xcffib.xproto.PropMode.Replace, window, wm_protocols, xcffib.xproto.Atom.ATOM, 32, 1, [wm_delete_window]) conn.core.ConfigureWindow(window, xcffib.xproto.ConfigWindow.X | xcffib.xproto.ConfigWindow.Y | xcffib.xproto.ConfigWindow.Width | xcffib.xproto.ConfigWindow.Height | xcffib.xproto.ConfigWindow.BorderWidth, [0, 0, 100, 100, 1]) conn.core.MapWindow(window) conn.flush() conn.core.ConfigureWindow(window, xcffib.xproto.ConfigWindow.X | xcffib.xproto.ConfigWindow.Y | xcffib.xproto.ConfigWindow.Width | xcffib.xproto.ConfigWindow.Height | xcffib.xproto.ConfigWindow.BorderWidth, [0, 0, 100, 100, 1]) try: while 1: event = conn.wait_for_event() if event.__class__ == xcffib.xproto.ClientMessageEvent: if conn.core.GetAtomName(event.data.data32[0]).reply().name.to_string() == "WM_DELETE_WINDOW": sys.exit(1) except xcffib.XcffibException: pass qtile-0.10.7/test/test_bar.py000066400000000000000000000275211305063162100160550ustar00rootroot00000000000000# Copyright (c) 2011 Florian Mounier # Copyright (c) 2012-2013 Craig Barnes # Copyright (c) 2012 roger # Copyright (c) 2012, 2014-2015 Tycho Andersen # Copyright (c) 2014 Sean Vig # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest import time import six import libqtile.layout import libqtile.bar import libqtile.widget import libqtile.manager import libqtile.config import libqtile.confreader class GBConfig(object): auto_fullscreen = True keys = [] mouse = [] groups = [ libqtile.config.Group("a"), libqtile.config.Group("bb"), libqtile.config.Group("ccc"), libqtile.config.Group("dddd"), libqtile.config.Group("Pppy") ] layouts = [libqtile.layout.stack.Stack(num_stacks=1)] floating_layout = libqtile.layout.floating.Floating() screens = [ libqtile.config.Screen( top=libqtile.bar.Bar( [ libqtile.widget.CPUGraph( width=libqtile.bar.STRETCH, type="linefill", border_width=20, margin_x=1, margin_y=1 ), libqtile.widget.MemoryGraph(type="line"), libqtile.widget.SwapGraph(type="box"), libqtile.widget.TextBox(name="text", background="333333"), ], 50, ), bottom=libqtile.bar.Bar( [ libqtile.widget.GroupBox(), libqtile.widget.AGroupBox(), libqtile.widget.Prompt(), libqtile.widget.WindowName(), libqtile.widget.Sep(), libqtile.widget.Clock(), ], 50 ), # TODO: Add vertical bars and test widgets that support them ) ] main = None gb_config = pytest.mark.parametrize("qtile", [GBConfig], indirect=True) def test_completion(): c = libqtile.widget.prompt.CommandCompleter(None, True) c.reset() c.lookup = [ ("a", "x/a"), ("aa", "x/aa"), ] assert c.complete("a") == "a" assert c.actual() == "x/a" assert c.complete("a") == "aa" assert c.complete("a") == "a" c = libqtile.widget.prompt.CommandCompleter(None) r = c.complete("l") assert c.actual().endswith(r) c.reset() assert c.complete("/bi") == "/bin/" c.reset() assert c.complete("/bin") != "/bin/" c.reset() assert c.complete("~") != "~" c.reset() s = "thisisatotallynonexistantpathforsure" assert c.complete(s) == s assert c.actual() == s c.reset() @gb_config def test_draw(qtile): qtile.testWindow("one") b = qtile.c.bar["bottom"].info() assert b["widgets"][0]["name"] == "groupbox" @gb_config def test_prompt(qtile): assert qtile.c.widget["prompt"].info()["width"] == 0 qtile.c.spawncmd(":") qtile.c.widget["prompt"].fake_keypress("a") qtile.c.widget["prompt"].fake_keypress("Tab") qtile.c.spawncmd(":") qtile.c.widget["prompt"].fake_keypress("slash") qtile.c.widget["prompt"].fake_keypress("Tab") @gb_config def test_event(qtile): qtile.c.group["bb"].toscreen() @gb_config def test_textbox(qtile): assert "text" in qtile.c.list_widgets() s = "some text" qtile.c.widget["text"].update(s) assert qtile.c.widget["text"].get() == s s = "Aye, much longer string than the initial one" qtile.c.widget["text"].update(s) assert qtile.c.widget["text"].get() == s qtile.c.group["Pppy"].toscreen() qtile.c.widget["text"].set_font(fontsize=12) time.sleep(3) @gb_config def test_textbox_errors(qtile): qtile.c.widget["text"].update(None) qtile.c.widget["text"].update("".join(chr(i) for i in range(255))) qtile.c.widget["text"].update("V\xE2r\xE2na\xE7\xEE") qtile.c.widget["text"].update(six.u("\ua000")) @gb_config def test_groupbox_button_press(qtile): qtile.c.group["ccc"].toscreen() assert qtile.c.groups()["a"]["screen"] is None qtile.c.bar["bottom"].fake_button_press(0, "bottom", 10, 10, 1) assert qtile.c.groups()["a"]["screen"] == 0 class GeomConf(object): auto_fullscreen = False main = None keys = [] mouse = [] groups = [ libqtile.config.Group("a"), libqtile.config.Group("b"), libqtile.config.Group("c"), libqtile.config.Group("d") ] layouts = [libqtile.layout.stack.Stack(num_stacks=1)] floating_layout = libqtile.layout.floating.Floating() screens = [ libqtile.config.Screen( top=libqtile.bar.Bar([], 10), bottom=libqtile.bar.Bar([], 10), left=libqtile.bar.Bar([], 10), right=libqtile.bar.Bar([], 10), ) ] geom_config = pytest.mark.parametrize("qtile", [GeomConf], indirect=True) class DBarH(libqtile.bar.Bar): def __init__(self, widgets, size): libqtile.bar.Bar.__init__(self, widgets, size) self.horizontal = True class DBarV(libqtile.bar.Bar): def __init__(self, widgets, size): libqtile.bar.Bar.__init__(self, widgets, size) self.horizontal = False class DWidget(object): def __init__(self, length, length_type): self.length, self.length_type = length, length_type @geom_config def test_geometry(qtile): qtile.testXeyes() g = qtile.c.screens()[0]["gaps"] assert g["top"] == (0, 0, 800, 10) assert g["bottom"] == (0, 590, 800, 10) assert g["left"] == (0, 10, 10, 580) assert g["right"] == (790, 10, 10, 580) assert len(qtile.c.windows()) == 1 geom = qtile.c.windows()[0] assert geom["x"] == 10 assert geom["y"] == 10 assert geom["width"] == 778 assert geom["height"] == 578 internal = qtile.c.internal_windows() assert len(internal) == 4 wid = qtile.c.bar["bottom"].info()["window"] assert qtile.c.window[wid].inspect() @geom_config def test_resize(qtile): def wd(l): return [i.length for i in l] def offx(l): return [i.offsetx for i in l] def offy(l): return [i.offsety for i in l] for DBar, off in ((DBarH, offx), (DBarV, offy)): b = DBar([], 100) l = [ DWidget(10, libqtile.bar.CALCULATED), DWidget(None, libqtile.bar.STRETCH), DWidget(None, libqtile.bar.STRETCH), DWidget(10, libqtile.bar.CALCULATED), ] b._resize(100, l) assert wd(l) == [10, 40, 40, 10] assert off(l) == [0, 10, 50, 90] b._resize(101, l) assert wd(l) == [10, 40, 41, 10] assert off(l) == [0, 10, 50, 91] l = [ DWidget(10, libqtile.bar.CALCULATED) ] b._resize(100, l) assert wd(l) == [10] assert off(l) == [0] l = [ DWidget(10, libqtile.bar.CALCULATED), DWidget(None, libqtile.bar.STRETCH) ] b._resize(100, l) assert wd(l) == [10, 90] assert off(l) == [0, 10] l = [ DWidget(None, libqtile.bar.STRETCH), DWidget(10, libqtile.bar.CALCULATED), ] b._resize(100, l) assert wd(l) == [90, 10] assert off(l) == [0, 90] l = [ DWidget(10, libqtile.bar.CALCULATED), DWidget(None, libqtile.bar.STRETCH), DWidget(10, libqtile.bar.CALCULATED), ] b._resize(100, l) assert wd(l) == [10, 80, 10] assert off(l) == [0, 10, 90] class ExampleWidget(libqtile.widget.base._Widget): orientations = libqtile.widget.base.ORIENTATION_HORIZONTAL def __init__(self): libqtile.widget.base._Widget.__init__(self, 10) def draw(self): pass class IncompatibleWidgetConf(object): main = None keys = [] mouse = [] groups = [libqtile.config.Group("a")] layouts = [libqtile.layout.stack.Stack(num_stacks=1)] floating_layout = libqtile.layout.floating.Floating() screens = [ libqtile.config.Screen( left=libqtile.bar.Bar( [ # This widget doesn't support vertical orientation ExampleWidget(), ], 10 ), ) ] def test_incompatible_widget(qtile_nospawn): config = IncompatibleWidgetConf # Ensure that adding a widget that doesn't support the orientation of the # bar raises ConfigError with pytest.raises(libqtile.confreader.ConfigError): qtile_nospawn.create_manager(config) class MultiStretchConf(object): main = None keys = [] mouse = [] groups = [libqtile.config.Group("a")] layouts = [libqtile.layout.stack.Stack(num_stacks=1)] floating_layout = libqtile.layout.floating.Floating() screens = [ libqtile.config.Screen( top=libqtile.bar.Bar( [ libqtile.widget.Spacer(libqtile.bar.STRETCH), libqtile.widget.Spacer(libqtile.bar.STRETCH), ], 10 ), ) ] def test_multiple_stretches(qtile_nospawn): config = MultiStretchConf # Ensure that adding two STRETCH widgets to the same bar raises ConfigError with pytest.raises(libqtile.confreader.ConfigError): qtile_nospawn.create_manager(config) def test_basic(qtile_nospawn): config = GeomConf config.screens = [ libqtile.config.Screen( bottom=libqtile.bar.Bar( [ ExampleWidget(), libqtile.widget.Spacer(libqtile.bar.STRETCH), ExampleWidget() ], 10 ) ) ] qtile_nospawn.start(config) i = qtile_nospawn.c.bar["bottom"].info() assert i["widgets"][0]["offset"] == 0 assert i["widgets"][1]["offset"] == 10 assert i["widgets"][1]["width"] == 780 assert i["widgets"][2]["offset"] == 790 libqtile.hook.clear() def test_singlespacer(qtile_nospawn): config = GeomConf config.screens = [ libqtile.config.Screen( bottom=libqtile.bar.Bar( [ libqtile.widget.Spacer(libqtile.bar.STRETCH), ], 10 ) ) ] qtile_nospawn.start(config) i = qtile_nospawn.c.bar["bottom"].info() assert i["widgets"][0]["offset"] == 0 assert i["widgets"][0]["width"] == 800 libqtile.hook.clear() def test_nospacer(qtile_nospawn): config = GeomConf config.screens = [ libqtile.config.Screen( bottom=libqtile.bar.Bar( [ ExampleWidget(), ExampleWidget() ], 10 ) ) ] qtile_nospawn.start(config) i = qtile_nospawn.c.bar["bottom"].info() assert i["widgets"][0]["offset"] == 0 assert i["widgets"][1]["offset"] == 10 libqtile.hook.clear() qtile-0.10.7/test/test_command.py000066400000000000000000000261501305063162100167240ustar00rootroot00000000000000# Copyright (c) 2011 Florian Mounier # Copyright (c) 2012, 2014 Tycho Andersen # Copyright (c) 2013 Craig Barnes # Copyright (c) 2014 Sean Vig # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest import libqtile import libqtile.confreader import libqtile.manager import libqtile.config import libqtile.layout import libqtile.bar import libqtile.widget class CallConfig(object): keys = [ libqtile.config.Key( ["control"], "j", libqtile.command._Call([("layout", None)], "down") ), libqtile.config.Key( ["control"], "k", libqtile.command._Call([("layout", None)], "up"), ), ] mouse = [] groups = [ libqtile.config.Group("a"), libqtile.config.Group("b"), ] layouts = [ libqtile.layout.Stack(num_stacks=1), libqtile.layout.Max(), ] floating_layout = libqtile.layout.floating.Floating() screens = [ libqtile.config.Screen( bottom=libqtile.bar.Bar( [ libqtile.widget.GroupBox(), ], 20 ), ) ] main = None auto_fullscreen = True call_config = pytest.mark.parametrize("qtile", [CallConfig], indirect=True) @call_config def test_layout_filter(qtile): qtile.testWindow("one") qtile.testWindow("two") assert qtile.c.groups()["a"]["focus"] == "two" qtile.c.simulate_keypress(["control"], "j") assert qtile.c.groups()["a"]["focus"] == "one" qtile.c.simulate_keypress(["control"], "k") assert qtile.c.groups()["a"]["focus"] == "two" class TestCommands(libqtile.command.CommandObject): @staticmethod def cmd_one(): pass def cmd_one_self(self): pass def cmd_two(self, a): pass def cmd_three(self, a, b=99): pass def _items(self, name): return None def _select(self, name, sel): return None def test_doc(): c = TestCommands() assert "one()" in c.doc("one") assert "one_self()" in c.doc("one_self") assert "two(a)" in c.doc("two") assert "three(a, b=99)" in c.doc("three") def test_commands(): c = TestCommands() assert len(c.cmd_commands()) == 9 def test_command(): c = TestCommands() assert c.command("one") assert not c.command("nonexistent") class ConcreteCmdRoot(libqtile.command._CommandRoot): def call(self, *args): return args def _items(self, name): return None def _select(self, name, sel): return None def test_selectors(): c = ConcreteCmdRoot() s = c.layout.screen.info assert s.selectors == [('layout', None), ('screen', None)] assert isinstance(c.info, libqtile.command._Command) g = c.group assert isinstance(g, libqtile.command._TGroup) assert g.myselector is None g = c.group["one"] assert isinstance(g, libqtile.command._TGroup) assert g.myselector == "one" cmd = c.group["one"].foo assert cmd.name == "foo" assert cmd.selectors == [('group', 'one')] g = c.group["two"].layout["three"].screen assert g.selectors == [('group', 'two'), ('layout', 'three')] g = c.one assert g.selectors == [] class ServerConfig(object): auto_fullscreen = True keys = [] mouse = [] groups = [ libqtile.config.Group("a"), libqtile.config.Group("b"), libqtile.config.Group("c"), ] layouts = [ libqtile.layout.Stack(num_stacks=1), libqtile.layout.Stack(num_stacks=2), libqtile.layout.Stack(num_stacks=3), ] floating_layout = libqtile.layout.floating.Floating() screens = [ libqtile.config.Screen( bottom=libqtile.bar.Bar( [ libqtile.widget.TextBox(name="one"), ], 20 ), ), libqtile.config.Screen( bottom=libqtile.bar.Bar( [ libqtile.widget.TextBox(name="two"), ], 20 ), ) ] main = None server_config = pytest.mark.parametrize("qtile", [ServerConfig], indirect=True) @server_config def test_cmd_commands(qtile): assert qtile.c.commands() assert qtile.c.layout.commands() assert qtile.c.screen.bar["bottom"].commands() @server_config def test_call_unknown(qtile): with pytest.raises(libqtile.command.CommandError): qtile.c.nonexistent() with pytest.raises(libqtile.command.CommandError): qtile.c.layout.nonexistent() @server_config def test_items_qtile(qtile): v = qtile.c.items("group") assert v[0] assert sorted(v[1]) == ["a", "b", "c"] assert qtile.c.items("layout") == (True, [0, 1, 2]) v = qtile.c.items("widget") assert not v[0] assert sorted(v[1]) == ['one', 'two'] assert qtile.c.items("bar") == (False, ["bottom"]) t, lst = qtile.c.items("window") assert t assert len(lst) == 2 assert qtile.c.window[lst[0]] assert qtile.c.items("screen") == (True, [0, 1]) @server_config def test_select_qtile(qtile): assert qtile.c.foo.selectors == [] assert qtile.c.layout.info()["group"] == "a" assert len(qtile.c.layout.info()["stacks"]) == 1 assert len(qtile.c.layout[2].info()["stacks"]) == 3 with pytest.raises(libqtile.command.CommandError): qtile.c.layout[99].info() assert qtile.c.group.info()["name"] == "a" assert qtile.c.group["c"].info()["name"] == "c" with pytest.raises(libqtile.command.CommandError): qtile.c.group["nonexistent"].info() assert qtile.c.widget["one"].info()["name"] == "one" with pytest.raises(libqtile.command.CommandError): qtile.c.widget.info() assert qtile.c.bar["bottom"].info()["position"] == "bottom" win = qtile.testWindow("one") wid = qtile.c.window.info()["id"] assert qtile.c.window[wid].info()["id"] == wid assert qtile.c.screen.info()["index"] == 0 assert qtile.c.screen[1].info()["index"] == 1 with pytest.raises(libqtile.command.CommandError): qtile.c.screen[22].info() with pytest.raises(libqtile.command.CommandError): qtile.c.screen["foo"].info() @server_config def test_items_group(qtile): g = qtile.c.group assert g.items("layout") == (True, [0, 1, 2]) win = qtile.testWindow("test") wid = qtile.c.window.info()["id"] assert g.items("window") == (True, [wid]) assert g.items("screen") == (True, None) @server_config def test_select_group(qtile): g = qtile.c.group assert g.layout.info()["group"] == "a" assert len(g.layout.info()["stacks"]) == 1 assert len(g.layout[2].info()["stacks"]) == 3 with pytest.raises(libqtile.command.CommandError): qtile.c.group.window.info() win = qtile.testWindow("test") wid = qtile.c.window.info()["id"] assert g.window.info()["id"] == wid assert g.window[wid].info()["id"] == wid with pytest.raises(libqtile.command.CommandError): g.window["foo"].info() assert g.screen.info()["index"] == 0 assert g["b"].screen.info()["index"] == 1 with pytest.raises(libqtile.command.CommandError): g["b"].screen[0].info() @server_config def test_items_screen(qtile): s = qtile.c.screen assert s.items("layout") == (True, [0, 1, 2]) win = qtile.testWindow("test") wid = qtile.c.window.info()["id"] assert s.items("window") == (True, [wid]) assert s.items("bar") == (False, ["bottom"]) @server_config def test_select_screen(qtile): s = qtile.c.screen assert s.layout.info()["group"] == "a" assert len(s.layout.info()["stacks"]) == 1 assert len(s.layout[2].info()["stacks"]) == 3 with pytest.raises(libqtile.command.CommandError): qtile.c.window.info() with pytest.raises(libqtile.command.CommandError): qtile.c.window[2].info() win = qtile.testWindow("test") wid = qtile.c.window.info()["id"] assert s.window.info()["id"] == wid assert s.window[wid].info()["id"] == wid with pytest.raises(libqtile.command.CommandError): s.bar.info() with pytest.raises(libqtile.command.CommandError): s.bar["top"].info() assert s.bar["bottom"].info()["position"] == "bottom" @server_config def test_items_bar(qtile): assert qtile.c.bar["bottom"].items("screen") == (True, None) @server_config def test_select_bar(qtile): assert qtile.c.screen[1].bar["bottom"].screen.info()["index"] == 1 b = qtile.c.bar assert b["bottom"].screen.info()["index"] == 0 with pytest.raises(libqtile.command.CommandError): b.screen.info() @server_config def test_items_layout(qtile): assert qtile.c.layout.items("screen") == (True, None) assert qtile.c.layout.items("group") == (True, None) @server_config def test_select_layout(qtile): assert qtile.c.layout.screen.info()["index"] == 0 with pytest.raises(libqtile.command.CommandError): qtile.c.layout.screen[0].info() assert qtile.c.layout.group.info()["name"] == "a" with pytest.raises(libqtile.command.CommandError): qtile.c.layout.group["a"].info() @server_config def test_items_window(qtile): win = qtile.testWindow("test") wid = qtile.c.window.info()["id"] assert qtile.c.window.items("group") == (True, None) assert qtile.c.window.items("layout") == (True, [0, 1, 2]) assert qtile.c.window.items("screen") == (True, None) @server_config def test_select_window(qtile): win = qtile.testWindow("test") wid = qtile.c.window.info()["id"] assert qtile.c.window.group.info()["name"] == "a" with pytest.raises(libqtile.command.CommandError): qtile.c.window.group["a"].info() assert len(qtile.c.window.layout.info()["stacks"]) == 1 assert len(qtile.c.window.layout[1].info()["stacks"]) == 2 assert qtile.c.window.screen.info()["index"] == 0 with pytest.raises(libqtile.command.CommandError): qtile.c.window.screen[0].info() @server_config def test_items_widget(qtile): assert qtile.c.widget["one"].items("bar") == (True, None) @server_config def test_select_widget(qtile): w = qtile.c.widget["one"] assert w.bar.info()["position"] == "bottom" with pytest.raises(libqtile.command.CommandError): w.bar["bottom"].info() qtile-0.10.7/test/test_config.py000066400000000000000000000056061305063162100165560ustar00rootroot00000000000000# Copyright (c) 2011 Florian Mounier # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Tycho Andersen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import os import pytest from libqtile import confreader from libqtile import config, utils tests_dir = os.path.dirname(os.path.realpath(__file__)) def test_syntaxerr(): with pytest.raises(confreader.ConfigError): confreader.File(os.path.join(tests_dir, "configs", "syntaxerr.py")) def test_basic(): f = confreader.File(os.path.join(tests_dir, "configs", "basic.py")) assert f.keys def test_falls_back(): f = confreader.File(os.path.join(tests_dir, "configs", "basic.py")) # We just care that it has a default, we don't actually care what the # default is; don't assert anything at all about the default in case # someone changes it down the road. assert hasattr(f, "follow_mouse_focus") def test_ezkey(): cmd = lambda x: None key = config.EzKey('M-A-S-a', cmd, cmd) modkey, altkey = (config.EzConfig.modifier_keys[i] for i in 'MA') assert key.modifiers == [modkey, altkey, 'shift'] assert key.key == 'a' assert key.commands == (cmd, cmd) key = config.EzKey('M-', cmd) assert key.modifiers == [modkey] assert key.key == 'Tab' assert key.commands == (cmd,) with pytest.raises(utils.QtileError): config.EzKey('M--', cmd) with pytest.raises(utils.QtileError): config.EzKey('Z-Z-z', cmd) with pytest.raises(utils.QtileError): config.EzKey('asdf', cmd) with pytest.raises(utils.QtileError): config.EzKey('M-a-A', cmd) def test_ezclick_ezdrag(): cmd = lambda x: None btn = config.EzClick('M-1', cmd) assert btn.button == 'Button1' assert btn.modifiers == [config.EzClick.modifier_keys['M']] btn = config.EzDrag('A-2', cmd) assert btn.button == 'Button2' assert btn.modifiers == [config.EzClick.modifier_keys['A']] qtile-0.10.7/test/test_configurable.py000066400000000000000000000045771305063162100177570ustar00rootroot00000000000000# Copyright (c) 2015 Michael Killough # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from libqtile import configurable class ConfigurableWithFallback(configurable.Configurable): defaults = [ ("foo", 3, ""), ] bar = configurable.ExtraFallback('bar', 'foo') def __init__(self, **config): configurable.Configurable.__init__(self, **config) self.add_defaults(self.defaults) def test_use_fallback(): c = ConfigurableWithFallback() assert c.foo == c.bar == 3 c = ConfigurableWithFallback(foo=5) assert c.foo == c.bar == 5 def test_use_fallback_if_set_to_none(): # Even if it is explicitly set to None, we should still # use the fallback. Could be useful if widget_defaults # were to set bar= and we wanted to specify that an # individual widget should fall back to using foo. c = ConfigurableWithFallback(foo=7, bar=None) assert c.foo == c.bar == 7 c = ConfigurableWithFallback(foo=9) c.bar = None assert c.foo == c.bar == 9 def test_dont_use_fallback_if_set(): c = ConfigurableWithFallback(bar=5) assert c.foo == 3 assert c.bar == 5 c = ConfigurableWithFallback(bar=0) assert c.foo == 3 assert c.bar == 0 c = ConfigurableWithFallback(foo=1, bar=2) assert c.foo == 1 assert c.bar == 2 c = ConfigurableWithFallback(foo=1) c.bar = 3 assert c.foo == 1 assert c.bar == 3 qtile-0.10.7/test/test_fakescreen.py000066400000000000000000000362541305063162100174220ustar00rootroot00000000000000# Copyright (c) 2011 Florian Mounier # Copyright (c) 2012, 2014 Tycho Andersen # Copyright (c) 2013 Craig Barnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Adi Sieker # Copyright (c) 2014 Sebastien Blot # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest import libqtile.manager import libqtile.config from libqtile import layout, bar, widget from libqtile.config import Screen LEFT_ALT = 'mod1' WINDOWS = 'mod4' FONTSIZE = 13 CHAM1 = '8AE234' CHAM3 = '4E9A06' GRAPH_KW = dict(line_width=1, graph_color=CHAM3, fill_color=CHAM3 + '.3', border_width=1, border_color=CHAM3 ) # screens look like this # 600 300 # |-------------|-----| # | 480| |580 # | A | B | # |----------|--| | # | 400|--|-----| # | C | |400 # |----------| D | # 500 |--------| # 400 # # Notice there is a hole in the middle # also D goes down below the others class FakeScreenConfig(object): auto_fullscreen = True main = None groups = [ libqtile.config.Group("a"), libqtile.config.Group("b"), libqtile.config.Group("c"), libqtile.config.Group("d") ] layouts = [ layout.Max(), layout.RatioTile(), layout.Tile(), ] floating_layout = libqtile.layout.floating.Floating() keys = [] mouse = [] fake_screens = [ Screen( bottom=bar.Bar( [ widget.GroupBox(this_screen_border=CHAM3, borderwidth=1, fontsize=FONTSIZE, padding=1, margin_x=1, margin_y=1), widget.AGroupBox(), widget.Prompt(), widget.Sep(), widget.WindowName(fontsize=FONTSIZE, margin_x=6), widget.Sep(), widget.CPUGraph(**GRAPH_KW), widget.MemoryGraph(**GRAPH_KW), widget.SwapGraph(foreground='20C020', **GRAPH_KW), widget.Sep(), widget.Systray(), widget.Sep(), widget.Clock(format='%H:%M:%S %d.%m.%Y', fontsize=FONTSIZE, padding=6), ], 24, background="#555555" ), left=bar.Gap(16), right=bar.Gap(20), x=0, y=0, width=600, height=480 ), Screen( top=bar.Bar( [ widget.GroupBox(), widget.WindowName(), widget.Clock() ], 30, ), bottom=bar.Gap(24), left=bar.Gap(12), x=600, y=0, width=300, height=580 ), Screen( top=bar.Bar( [ widget.GroupBox(), widget.WindowName(), widget.Clock() ], 30, ), bottom=bar.Gap(16), right=bar.Gap(40), x=0, y=480, width=500, height=400 ), Screen( top=bar.Bar( [ widget.GroupBox(), widget.WindowName(), widget.Clock() ], 30, ), left=bar.Gap(20), right=bar.Gap(24), x=500, y=580, width=400, height=400 ), ] screens = fake_screens xephyr_config = { "xinerama": False, "two_screens": False, "width": 900, "height": 980 } fakescreen_config = pytest.mark.parametrize("xephyr, qtile", [(xephyr_config, FakeScreenConfig)], indirect=True) @fakescreen_config def test_basic(qtile): qtile.testWindow("zero") assert qtile.c.layout.info()["clients"] == ["zero"] assert qtile.c.screen.info() == { 'y': 0, 'x': 0, 'index': 0, 'width': 600, 'height': 480} qtile.c.to_screen(1) qtile.testWindow("one") assert qtile.c.layout.info()["clients"] == ["one"] assert qtile.c.screen.info() == { 'y': 0, 'x': 600, 'index': 1, 'width': 300, 'height': 580} qtile.c.to_screen(2) qtile.testXeyes() assert qtile.c.screen.info() == { 'y': 480, 'x': 0, 'index': 2, 'width': 500, 'height': 400} qtile.c.to_screen(3) qtile.testXclock() assert qtile.c.screen.info() == { 'y': 580, 'x': 500, 'index': 3, 'width': 400, 'height': 400} @fakescreen_config def test_gaps(qtile): g = qtile.c.screens()[0]["gaps"] assert g["bottom"] == (0, 456, 600, 24) assert g["left"] == (0, 0, 16, 456) assert g["right"] == (580, 0, 20, 456) g = qtile.c.screens()[1]["gaps"] assert g["top"] == (600, 0, 300, 30) assert g["bottom"] == (600, 556, 300, 24) assert g["left"] == (600, 30, 12, 526) g = qtile.c.screens()[2]["gaps"] assert g["top"] == (0, 480, 500, 30) assert g["bottom"] == (0, 864, 500, 16) assert g["right"] == (460, 510, 40, 354) g = qtile.c.screens()[3]["gaps"] assert g["top"] == (500, 580, 400, 30) assert g["left"] == (500, 610, 20, 370) assert g["right"] == (876, 610, 24, 370) @fakescreen_config def test_maximize_with_move_to_screen(qtile): """Ensure that maximize respects bars""" qtile.testXclock() qtile.c.window.toggle_maximize() assert qtile.c.window.info()['width'] == 564 assert qtile.c.window.info()['height'] == 456 assert qtile.c.window.info()['x'] == 16 assert qtile.c.window.info()['y'] == 0 assert qtile.c.window.info()['group'] == 'a' # go to second screen qtile.c.to_screen(1) assert qtile.c.screen.info() == { 'y': 0, 'x': 600, 'index': 1, 'width': 300, 'height': 580} assert qtile.c.group.info()['name'] == 'b' qtile.c.group['a'].toscreen() assert qtile.c.window.info()['width'] == 288 assert qtile.c.window.info()['height'] == 526 assert qtile.c.window.info()['x'] == 612 assert qtile.c.window.info()['y'] == 30 assert qtile.c.window.info()['group'] == 'a' @fakescreen_config def test_float_first_on_second_screen(qtile): qtile.c.to_screen(1) assert qtile.c.screen.info() == { 'y': 0, 'x': 600, 'index': 1, 'width': 300, 'height': 580} qtile.testXclock() # I don't know where y=30, x=12 comes from... assert qtile.c.window.info()['float_info'] == { 'y': 30, 'x': 12, 'width': 164, 'height': 164 } qtile.c.window.toggle_floating() assert qtile.c.window.info()['width'] == 164 assert qtile.c.window.info()['height'] == 164 assert qtile.c.window.info()['x'] == 612 assert qtile.c.window.info()['y'] == 30 assert qtile.c.window.info()['group'] == 'b' assert qtile.c.window.info()['float_info'] == { 'y': 30, 'x': 12, 'width': 164, 'height': 164 } @fakescreen_config def test_float_change_screens(qtile): # add some eyes, and float clock qtile.testXeyes() qtile.testXclock() qtile.c.window.toggle_floating() assert set(qtile.c.group.info()['windows']) == set(('xeyes', 'xclock')) assert qtile.c.group.info()['floating_info']['clients'] == ['xclock'] assert qtile.c.window.info()['width'] == 164 assert qtile.c.window.info()['height'] == 164 # 16 is given by the left gap width assert qtile.c.window.info()['x'] == 16 assert qtile.c.window.info()['y'] == 0 assert qtile.c.window.info()['group'] == 'a' # put on group b assert qtile.c.screen.info() == { 'y': 0, 'x': 0, 'index': 0, 'width': 600, 'height': 480} assert qtile.c.group.info()['name'] == 'a' qtile.c.to_screen(1) assert qtile.c.group.info()['name'] == 'b' assert qtile.c.screen.info() == { 'y': 0, 'x': 600, 'index': 1, 'width': 300, 'height': 580} qtile.c.group['a'].toscreen() assert qtile.c.group.info()['name'] == 'a' assert set(qtile.c.group.info()['windows']) == set(('xeyes', 'xclock')) assert qtile.c.window.info()['name'] == 'xclock' # width/height unchanged assert qtile.c.window.info()['width'] == 164 assert qtile.c.window.info()['height'] == 164 # x is shifted by 600, y is shifted by 0 assert qtile.c.window.info()['x'] == 616 assert qtile.c.window.info()['y'] == 0 assert qtile.c.window.info()['group'] == 'a' assert qtile.c.group.info()['floating_info']['clients'] == ['xclock'] # move to screen 3 qtile.c.to_screen(2) assert qtile.c.screen.info() == { 'y': 480, 'x': 0, 'index': 2, 'width': 500, 'height': 400} assert qtile.c.group.info()['name'] == 'c' qtile.c.group['a'].toscreen() assert qtile.c.group.info()['name'] == 'a' assert set(qtile.c.group.info()['windows']) == set(('xeyes', 'xclock')) assert qtile.c.window.info()['name'] == 'xclock' # width/height unchanged assert qtile.c.window.info()['width'] == 164 assert qtile.c.window.info()['height'] == 164 # x is shifted by 0, y is shifted by 480 assert qtile.c.window.info()['x'] == 16 assert qtile.c.window.info()['y'] == 480 # now screen 4 for fun qtile.c.to_screen(3) assert qtile.c.screen.info() == { 'y': 580, 'x': 500, 'index': 3, 'width': 400, 'height': 400} assert qtile.c.group.info()['name'] == 'd' qtile.c.group['a'].toscreen() assert qtile.c.group.info()['name'] == 'a' assert set(qtile.c.group.info()['windows']) == set(('xeyes', 'xclock')) assert qtile.c.window.info()['name'] == 'xclock' # width/height unchanged assert qtile.c.window.info()['width'] == 164 assert qtile.c.window.info()['height'] == 164 # x is shifted by 500, y is shifted by 580 assert qtile.c.window.info()['x'] == 516 assert qtile.c.window.info()['y'] == 580 # and back to one qtile.c.to_screen(0) assert qtile.c.screen.info() == { 'y': 0, 'x': 0, 'index': 0, 'width': 600, 'height': 480} assert qtile.c.group.info()['name'] == 'b' qtile.c.group['a'].toscreen() assert qtile.c.group.info()['name'] == 'a' assert set(qtile.c.group.info()['windows']) == set(('xeyes', 'xclock')) assert qtile.c.window.info()['name'] == 'xclock' # back to the original location assert qtile.c.window.info()['width'] == 164 assert qtile.c.window.info()['height'] == 164 assert qtile.c.window.info()['x'] == 16 assert qtile.c.window.info()['y'] == 0 @fakescreen_config def test_float_outside_edges(qtile): qtile.testXclock() qtile.c.window.toggle_floating() assert qtile.c.window.info()['width'] == 164 assert qtile.c.window.info()['height'] == 164 # 16 is given by the left gap width assert qtile.c.window.info()['x'] == 16 assert qtile.c.window.info()['y'] == 0 # empty because window is floating assert qtile.c.layout.info() == { 'clients': [], 'group': 'a', 'name': 'max'} # move left, but some still on screen 0 qtile.c.window.move_floating(-30, 20, 42, 42) assert qtile.c.window.info()['width'] == 164 assert qtile.c.window.info()['height'] == 164 assert qtile.c.window.info()['x'] == -14 assert qtile.c.window.info()['y'] == 20 assert qtile.c.window.info()['group'] == 'a' # move up, but some still on screen 0 qtile.c.window.set_position_floating(-10, -20, 42, 42) assert qtile.c.window.info()['width'] == 164 assert qtile.c.window.info()['height'] == 164 assert qtile.c.window.info()['x'] == -10 assert qtile.c.window.info()['y'] == -20 assert qtile.c.window.info()['group'] == 'a' # move above a qtile.c.window.set_position_floating(50, -20, 42, 42) assert qtile.c.window.info()['width'] == 164 assert qtile.c.window.info()['height'] == 164 assert qtile.c.window.info()['x'] == 50 assert qtile.c.window.info()['y'] == -20 assert qtile.c.window.info()['group'] == 'a' # move down so still left, but next to screen c qtile.c.window.set_position_floating(-10, 520, 42, 42) assert qtile.c.window.info()['height'] == 164 assert qtile.c.window.info()['x'] == -10 assert qtile.c.window.info()['y'] == 520 assert qtile.c.window.info()['group'] == 'c' # move above b qtile.c.window.set_position_floating(700, -10, 42, 42) assert qtile.c.window.info()['width'] == 164 assert qtile.c.window.info()['height'] == 164 assert qtile.c.window.info()['x'] == 700 assert qtile.c.window.info()['y'] == -10 assert qtile.c.window.info()['group'] == 'b' @fakescreen_config def test_hammer_tile(qtile): # change to tile layout qtile.c.next_layout() qtile.c.next_layout() for i in range(7): qtile.testXclock() for i in range(30): old_group = (i + 1) % 4 if old_group == 0: name = 'a' elif old_group == 1: name = 'b' elif old_group == 2: name = 'c' elif old_group == 3: name = 'd' qtile.c.to_screen((i + 1) % 4) qtile.c.group['a'].toscreen() assert qtile.c.group['a'].info()['windows'] == [ 'xclock', 'xclock', 'xclock', 'xclock', 'xclock', 'xclock', 'xclock'] @fakescreen_config def test_hammer_ratio_tile(qtile): # change to ratio tile layout qtile.c.next_layout() for i in range(7): qtile.testXclock() for i in range(30): old_group = (i + 1) % 4 if old_group == 0: name = 'a' elif old_group == 1: name = 'b' elif old_group == 2: name = 'c' elif old_group == 3: name = 'd' qtile.c.to_screen((i + 1) % 4) qtile.c.group['a'].toscreen() assert qtile.c.group['a'].info()['windows'] == [ 'xclock', 'xclock', 'xclock', 'xclock', 'xclock', 'xclock', 'xclock'] @fakescreen_config def test_ratio_to_fourth_screen(qtile): # change to ratio tile layout qtile.c.next_layout() for i in range(7): qtile.testXclock() qtile.c.to_screen(1) qtile.c.group['a'].toscreen() assert qtile.c.group['a'].info()['windows'] == [ 'xclock', 'xclock', 'xclock', 'xclock', 'xclock', 'xclock', 'xclock'] # now move to 4th, fails... qtile.c.to_screen(3) qtile.c.group['a'].toscreen() assert qtile.c.group['a'].info()['windows'] == [ 'xclock', 'xclock', 'xclock', 'xclock', 'xclock', 'xclock', 'xclock'] qtile-0.10.7/test/test_hook.py000066400000000000000000000073241305063162100162500ustar00rootroot00000000000000# Copyright (c) 2009 Aldo Cortesi # Copyright (c) 2011 Florian Mounier # Copyright (c) 2011 Anshuman Bhaduri # Copyright (c) 2012 Tycho Andersen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest from multiprocessing import Value import libqtile.log_utils import libqtile.manager import libqtile.utils import libqtile.hook import logging from .conftest import BareConfig # TODO: more tests required. # 1. Check all hooks that can be fired class Call(object): def __init__(self, val): self.val = val def __call__(self, val): self.val = val @pytest.yield_fixture def hook_fixture(): class Dummy(object): pass dummy = Dummy() libqtile.log_utils.init_log(logging.CRITICAL) libqtile.hook.init(dummy) yield libqtile.hook.clear() def test_cannot_fire_unknown_event(): with pytest.raises(libqtile.utils.QtileError): libqtile.hook.fire("unknown") @pytest.mark.usefixtures("hook_fixture") def test_hook_calls_subscriber(): test = Call(0) libqtile.manager.hook.subscribe.group_window_add(test) libqtile.manager.hook.fire("group_window_add", 8) assert test.val == 8 @pytest.mark.usefixtures("hook_fixture") def test_subscribers_can_be_added_removed(): test = Call(0) libqtile.manager.hook.subscribe.group_window_add(test) assert libqtile.manager.hook.subscriptions libqtile.manager.hook.clear() assert not libqtile.manager.hook.subscriptions @pytest.mark.usefixtures("hook_fixture") def test_can_unsubscribe_from_hook(): test = Call(0) libqtile.manager.hook.subscribe.group_window_add(test) libqtile.manager.hook.fire("group_window_add", 3) assert test.val == 3 libqtile.manager.hook.unsubscribe.group_window_add(test) libqtile.manager.hook.fire("group_window_add", 4) assert test.val == 3 def test_can_subscribe_to_startup_hooks(qtile_nospawn): config = BareConfig self = qtile_nospawn self.startup_once_calls = Value('i', 0) self.startup_calls = Value('i', 0) self.startup_complete_calls = Value('i', 0) def inc_startup_once_calls(): self.startup_once_calls.value += 1 def inc_startup_calls(): self.startup_calls.value += 1 def inc_startup_complete_calls(): self.startup_complete_calls.value += 1 libqtile.manager.hook.subscribe.startup_once(inc_startup_once_calls) libqtile.manager.hook.subscribe.startup(inc_startup_calls) libqtile.manager.hook.subscribe.startup_complete(inc_startup_complete_calls) self.start(config) self.start_qtile = True assert self.startup_once_calls.value == 1 assert self.startup_calls.value == 1 assert self.startup_complete_calls.value == 1 # TODO Restart and check that startup_once doesn't fire again qtile-0.10.7/test/test_manager.py000066400000000000000000000735621305063162100167310ustar00rootroot00000000000000# Copyright (c) 2011 Florian Mounier # Copyright (c) 2011 Anshuman Bhaduri # Copyright (c) 2012-2014 Tycho Andersen # Copyright (c) 2013 xarvh # Copyright (c) 2013 Craig Barnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 Adi Sieker # Copyright (c) 2014 Sebastien Blot # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest import subprocess import time import libqtile import libqtile.layout import libqtile.bar import libqtile.command import libqtile.widget import libqtile.manager import libqtile.config import libqtile.hook import libqtile.confreader from .conftest import whereis, BareConfig, no_xinerama class ManagerConfig(object): auto_fullscreen = True groups = [ libqtile.config.Group("a"), libqtile.config.Group("b"), libqtile.config.Group("c"), libqtile.config.Group("d") ] layouts = [ libqtile.layout.stack.Stack(num_stacks=1), libqtile.layout.stack.Stack(num_stacks=2), libqtile.layout.tile.Tile(ratio=0.5), libqtile.layout.max.Max() ] floating_layout = libqtile.layout.floating.Floating( float_rules=[dict(wmclass="xclock")]) keys = [ libqtile.config.Key( ["control"], "k", libqtile.command._Call([("layout", None)], "up") ), libqtile.config.Key( ["control"], "j", libqtile.command._Call([("layout", None)], "down") ), ] mouse = [] screens = [libqtile.config.Screen( bottom=libqtile.bar.Bar( [ libqtile.widget.GroupBox(), ], 20 ), )] main = None follow_mouse_focus = True manager_config = pytest.mark.parametrize("qtile", [ManagerConfig], indirect=True) @manager_config def test_screen_dim(qtile): # self.c.restart() qtile.testXclock() assert qtile.c.screen.info()["index"] == 0 assert qtile.c.screen.info()["x"] == 0 assert qtile.c.screen.info()["width"] == 800 assert qtile.c.group.info()["name"] == 'a' assert qtile.c.group.info()["focus"] == 'xclock' qtile.c.to_screen(1) qtile.testXeyes() assert qtile.c.screen.info()["index"] == 1 assert qtile.c.screen.info()["x"] == 800 assert qtile.c.screen.info()["width"] == 640 assert qtile.c.group.info()["name"] == 'b' assert qtile.c.group.info()["focus"] == 'xeyes' qtile.c.to_screen(0) assert qtile.c.screen.info()["index"] == 0 assert qtile.c.screen.info()["x"] == 0 assert qtile.c.screen.info()["width"] == 800 assert qtile.c.group.info()["name"] == 'a' assert qtile.c.group.info()["focus"] == 'xclock' @pytest.mark.parametrize("xephyr", [{"xoffset": 0}], indirect=True) @manager_config def test_clone_dim(qtile): self = qtile self.testXclock() assert self.c.screen.info()["index"] == 0 assert self.c.screen.info()["x"] == 0 assert self.c.screen.info()["width"] == 800 assert self.c.group.info()["name"] == 'a' assert self.c.group.info()["focus"] == 'xclock' assert len(self.c.screens()) == 1 @manager_config def test_to_screen(qtile): self = qtile assert self.c.screen.info()["index"] == 0 self.c.to_screen(1) assert self.c.screen.info()["index"] == 1 self.testWindow("one") self.c.to_screen(0) self.testWindow("two") ga = self.c.groups()["a"] assert ga["windows"] == ["two"] gb = self.c.groups()["b"] assert gb["windows"] == ["one"] assert self.c.window.info()["name"] == "two" self.c.next_screen() assert self.c.window.info()["name"] == "one" self.c.next_screen() assert self.c.window.info()["name"] == "two" self.c.prev_screen() assert self.c.window.info()["name"] == "one" @manager_config def test_togroup(qtile): self = qtile self.testWindow("one") with pytest.raises(libqtile.command.CommandError): self.c.window.togroup("nonexistent") assert self.c.groups()["a"]["focus"] == "one" self.c.window.togroup("a") assert self.c.groups()["a"]["focus"] == "one" self.c.window.togroup("b") assert self.c.groups()["b"]["focus"] == "one" assert self.c.groups()["a"]["focus"] is None self.c.to_screen(1) self.c.window.togroup("c") assert self.c.groups()["c"]["focus"] == "one" @manager_config def test_resize(qtile): self = qtile self.c.screen[0].resize(x=10, y=10, w=100, h=100) for _ in range(10): time.sleep(0.1) d = self.c.screen[0].info() if d["width"] == d["height"] == 100: break else: raise AssertionError("Screen didn't resize") assert d["x"] == d["y"] == 10 @no_xinerama def test_minimal(qtile): assert qtile.c.status() == "OK" @manager_config @no_xinerama def test_events(qtile): assert qtile.c.status() == "OK" # FIXME: failing test disabled. For some reason we don't seem # to have a keymap in Xnest or Xephyr 99% of the time. @manager_config @no_xinerama def test_keypress(qtile): self = qtile self.testWindow("one") self.testWindow("two") v = self.c.simulate_keypress(["unknown"], "j") assert v.startswith("Unknown modifier") assert self.c.groups()["a"]["focus"] == "two" self.c.simulate_keypress(["control"], "j") assert self.c.groups()["a"]["focus"] == "one" @manager_config @no_xinerama def test_spawn(qtile): # Spawn something with a pid greater than init's assert int(qtile.c.spawn("true")) > 1 @manager_config @no_xinerama def test_spawn_list(qtile): # Spawn something with a pid greater than init's assert int(qtile.c.spawn(["echo", "true"])) > 1 @manager_config @no_xinerama def test_kill_window(qtile): qtile.testWindow("one") qtile.testwindows = [] qtile.c.window[qtile.c.window.info()["id"]].kill() qtile.c.sync() for _ in range(20): time.sleep(0.1) if not qtile.c.windows(): break else: raise AssertionError("Window did not die...") @manager_config @no_xinerama def test_kill_other(qtile): self = qtile self.c.group.setlayout("tile") one = self.testWindow("one") assert self.c.window.info()["width"] == 798 assert self.c.window.info()["height"] == 578 two = self.testWindow("two") assert self.c.window.info()["name"] == "two" assert self.c.window.info()["width"] == 398 assert self.c.window.info()["height"] == 578 assert len(self.c.windows()) == 2 self.kill_window(one) for _ in range(10): time.sleep(0.1) if len(self.c.windows()) == 1: break else: raise AssertionError("window did not die") assert self.c.window.info()["name"] == "two" assert self.c.window.info()["width"] == 798 assert self.c.window.info()["height"] == 578 @manager_config @no_xinerama def test_regression_groupswitch(qtile): self = qtile self.c.group["c"].toscreen() self.c.group["d"].toscreen() assert self.c.groups()["c"]["screen"] is None @manager_config @no_xinerama def test_next_layout(qtile): self = qtile self.testWindow("one") self.testWindow("two") assert len(self.c.layout.info()["stacks"]) == 1 self.c.next_layout() assert len(self.c.layout.info()["stacks"]) == 2 self.c.next_layout() self.c.next_layout() self.c.next_layout() assert len(self.c.layout.info()["stacks"]) == 1 @manager_config @no_xinerama def test_setlayout(qtile): self = qtile assert not self.c.layout.info()["name"] == "max" self.c.group.setlayout("max") assert self.c.layout.info()["name"] == "max" @manager_config @no_xinerama def test_adddelgroup(qtile): self = qtile self.testWindow("one") self.c.addgroup("dummygroup") self.c.addgroup("testgroup") assert "testgroup" in self.c.groups().keys() self.c.window.togroup("testgroup") self.c.delgroup("testgroup") assert "testgroup" not in self.c.groups().keys() # Assert that the test window is still a member of some group. assert sum(len(i["windows"]) for i in self.c.groups().values()) for i in list(self.c.groups().keys())[:-1]: self.c.delgroup(i) with pytest.raises(libqtile.command.CommandException): self.c.delgroup(list(self.c.groups().keys())[0]) @manager_config @no_xinerama def test_delgroup(qtile): self = qtile self.testWindow("one") for i in ['a', 'd', 'c']: self.c.delgroup(i) with pytest.raises(libqtile.command.CommandException): self.c.delgroup('b') @manager_config @no_xinerama def test_nextprevgroup(qtile): self = qtile start = self.c.group.info()["name"] ret = self.c.screen.next_group() assert self.c.group.info()["name"] != start assert self.c.group.info()["name"] == ret ret = self.c.screen.prev_group() assert self.c.group.info()["name"] == start @manager_config @no_xinerama def test_toggle_group(qtile): self = qtile self.c.group["a"].toscreen() self.c.group["b"].toscreen() self.c.screen.toggle_group("c") assert self.c.group.info()["name"] == "c" self.c.screen.toggle_group("c") assert self.c.group.info()["name"] == "b" self.c.screen.toggle_group() assert self.c.group.info()["name"] == "c" @manager_config @no_xinerama def test_inspect_xeyes(qtile): self = qtile self.testXeyes() assert self.c.window.inspect() @manager_config @no_xinerama def test_inspect_xterm(qtile): self = qtile self.testXterm() assert self.c.window.inspect()["wm_class"] @manager_config @no_xinerama def test_static(qtile): self = qtile self.testXeyes() self.testWindow("one") self.c.window[self.c.window.info()["id"]].static(0, 0, 0, 100, 100) @manager_config @no_xinerama def test_match(qtile): self = qtile self.testXeyes() assert self.c.window.match(wname="xeyes") assert not self.c.window.match(wname="nonexistent") @manager_config @no_xinerama def test_default_float(qtile): self = qtile # change to 2 col stack self.c.next_layout() assert len(self.c.layout.info()["stacks"]) == 2 self.testXclock() assert self.c.group.info()['focus'] == 'xclock' assert self.c.window.info()['width'] == 164 assert self.c.window.info()['height'] == 164 assert self.c.window.info()['x'] == 0 assert self.c.window.info()['y'] == 0 assert self.c.window.info()['floating'] is True self.c.window.move_floating(10, 20, 42, 42) assert self.c.window.info()['width'] == 164 assert self.c.window.info()['height'] == 164 assert self.c.window.info()['x'] == 10 assert self.c.window.info()['y'] == 20 assert self.c.window.info()['floating'] is True @manager_config @no_xinerama def test_last_float_size(qtile): """ When you re-float something it would be preferable to have it use the previous float size """ self = qtile self.testXeyes() assert self.c.window.info()['name'] == 'xeyes' assert self.c.window.info()['width'] == 798 assert self.c.window.info()['height'] == 578 # float and it moves self.c.window.toggle_floating() assert self.c.window.info()['width'] == 150 assert self.c.window.info()['height'] == 100 # resize self.c.window.set_size_floating(50, 90, 42, 42) assert self.c.window.info()['width'] == 50 assert self.c.window.info()['height'] == 90 # back to not floating self.c.window.toggle_floating() assert self.c.window.info()['width'] == 798 assert self.c.window.info()['height'] == 578 # float again, should use last float size self.c.window.toggle_floating() assert self.c.window.info()['width'] == 50 assert self.c.window.info()['height'] == 90 # make sure it works through min and max self.c.window.toggle_maximize() self.c.window.toggle_minimize() self.c.window.toggle_minimize() self.c.window.toggle_floating() assert self.c.window.info()['width'] == 50 assert self.c.window.info()['height'] == 90 @manager_config @no_xinerama def test_float_max_min_combo(qtile): self = qtile # change to 2 col stack self.c.next_layout() assert len(self.c.layout.info()["stacks"]) == 2 self.testXterm() self.testXeyes() assert self.c.group.info()['focus'] == 'xeyes' assert self.c.window.info()['width'] == 398 assert self.c.window.info()['height'] == 578 assert self.c.window.info()['x'] == 400 assert self.c.window.info()['y'] == 0 assert self.c.window.info()['floating'] is False self.c.window.toggle_maximize() assert self.c.window.info()['floating'] is True assert self.c.window.info()['maximized'] is True assert self.c.window.info()['width'] == 800 assert self.c.window.info()['height'] == 580 assert self.c.window.info()['x'] == 0 assert self.c.window.info()['y'] == 0 self.c.window.toggle_minimize() assert self.c.group.info()['focus'] == 'xeyes' assert self.c.window.info()['floating'] is True assert self.c.window.info()['minimized'] is True assert self.c.window.info()['width'] == 800 assert self.c.window.info()['height'] == 580 assert self.c.window.info()['x'] == 0 assert self.c.window.info()['y'] == 0 self.c.window.toggle_floating() assert self.c.group.info()['focus'] == 'xeyes' assert self.c.window.info()['floating'] is False assert self.c.window.info()['minimized'] is False assert self.c.window.info()['maximized'] is False assert self.c.window.info()['width'] == 398 assert self.c.window.info()['height'] == 578 assert self.c.window.info()['x'] == 400 assert self.c.window.info()['y'] == 0 @manager_config @no_xinerama def test_toggle_fullscreen(qtile): self = qtile # change to 2 col stack self.c.next_layout() assert len(self.c.layout.info()["stacks"]) == 2 self.testXterm() self.testXeyes() assert self.c.group.info()['focus'] == 'xeyes' assert self.c.window.info()['width'] == 398 assert self.c.window.info()['height'] == 578 assert self.c.window.info()['float_info'] == { 'y': 0, 'x': 400, 'width': 150, 'height': 100} assert self.c.window.info()['x'] == 400 assert self.c.window.info()['y'] == 0 self.c.window.toggle_fullscreen() assert self.c.window.info()['floating'] is True assert self.c.window.info()['maximized'] is False assert self.c.window.info()['fullscreen'] is True assert self.c.window.info()['width'] == 800 assert self.c.window.info()['height'] == 600 assert self.c.window.info()['x'] == 0 assert self.c.window.info()['y'] == 0 self.c.window.toggle_fullscreen() assert self.c.window.info()['floating'] is False assert self.c.window.info()['maximized'] is False assert self.c.window.info()['fullscreen'] is False assert self.c.window.info()['width'] == 398 assert self.c.window.info()['height'] == 578 assert self.c.window.info()['x'] == 400 assert self.c.window.info()['y'] == 0 @manager_config @no_xinerama def test_toggle_max(qtile): self = qtile # change to 2 col stack self.c.next_layout() assert len(self.c.layout.info()["stacks"]) == 2 self.testXterm() self.testXeyes() assert self.c.group.info()['focus'] == 'xeyes' assert self.c.window.info()['width'] == 398 assert self.c.window.info()['height'] == 578 assert self.c.window.info()['float_info'] == { 'y': 0, 'x': 400, 'width': 150, 'height': 100} assert self.c.window.info()['x'] == 400 assert self.c.window.info()['y'] == 0 self.c.window.toggle_maximize() assert self.c.window.info()['floating'] is True assert self.c.window.info()['maximized'] is True assert self.c.window.info()['width'] == 800 assert self.c.window.info()['height'] == 580 assert self.c.window.info()['x'] == 0 assert self.c.window.info()['y'] == 0 self.c.window.toggle_maximize() assert self.c.window.info()['floating'] is False assert self.c.window.info()['maximized'] is False assert self.c.window.info()['width'] == 398 assert self.c.window.info()['height'] == 578 assert self.c.window.info()['x'] == 400 assert self.c.window.info()['y'] == 0 @manager_config @no_xinerama def test_toggle_min(qtile): self = qtile # change to 2 col stack self.c.next_layout() assert len(self.c.layout.info()["stacks"]) == 2 self.testXterm() self.testXeyes() assert self.c.group.info()['focus'] == 'xeyes' assert self.c.window.info()['width'] == 398 assert self.c.window.info()['height'] == 578 assert self.c.window.info()['float_info'] == { 'y': 0, 'x': 400, 'width': 150, 'height': 100} assert self.c.window.info()['x'] == 400 assert self.c.window.info()['y'] == 0 self.c.window.toggle_minimize() assert self.c.group.info()['focus'] == 'xeyes' assert self.c.window.info()['floating'] is True assert self.c.window.info()['minimized'] is True assert self.c.window.info()['width'] == 398 assert self.c.window.info()['height'] == 578 assert self.c.window.info()['x'] == 400 assert self.c.window.info()['y'] == 0 self.c.window.toggle_minimize() assert self.c.group.info()['focus'] == 'xeyes' assert self.c.window.info()['floating'] is False assert self.c.window.info()['minimized'] is False assert self.c.window.info()['width'] == 398 assert self.c.window.info()['height'] == 578 assert self.c.window.info()['x'] == 400 assert self.c.window.info()['y'] == 0 @manager_config @no_xinerama def test_toggle_floating(qtile): self = qtile self.testXeyes() assert self.c.window.info()['floating'] is False self.c.window.toggle_floating() assert self.c.window.info()['floating'] is True self.c.window.toggle_floating() assert self.c.window.info()['floating'] is False self.c.window.toggle_floating() assert self.c.window.info()['floating'] is True # change layout (should still be floating) self.c.next_layout() assert self.c.window.info()['floating'] is True @manager_config @no_xinerama def test_floating_focus(qtile): self = qtile # change to 2 col stack self.c.next_layout() assert len(self.c.layout.info()["stacks"]) == 2 self.testXterm() self.testXeyes() # self.testWindow("one") assert self.c.window.info()['width'] == 398 assert self.c.window.info()['height'] == 578 self.c.window.toggle_floating() self.c.window.move_floating(10, 20, 42, 42) assert self.c.window.info()['name'] == 'xeyes' assert self.c.group.info()['focus'] == 'xeyes' # check what stack thinks is focus assert [x['current'] for x in self.c.layout.info()['stacks']] == [0, 0] # change focus to xterm self.c.group.next_window() assert self.c.window.info()['width'] == 398 assert self.c.window.info()['height'] == 578 assert self.c.window.info()['name'] != 'xeyes' assert self.c.group.info()['focus'] != 'xeyes' # check what stack thinks is focus # check what stack thinks is focus assert [x['current'] for x in self.c.layout.info()['stacks']] == [0, 0] # focus back to xeyes self.c.group.next_window() assert self.c.window.info()['name'] == 'xeyes' # check what stack thinks is focus assert [x['current'] for x in self.c.layout.info()['stacks']] == [0, 0] # now focusing via layout is borked (won't go to float) self.c.layout.up() assert self.c.window.info()['name'] != 'xeyes' self.c.layout.up() assert self.c.window.info()['name'] != 'xeyes' # check what stack thinks is focus assert [x['current'] for x in self.c.layout.info()['stacks']] == [0, 0] # focus back to xeyes self.c.group.next_window() assert self.c.window.info()['name'] == 'xeyes' # check what stack thinks is focus assert [x['current'] for x in self.c.layout.info()['stacks']] == [0, 0] @manager_config @no_xinerama def test_move_floating(qtile): self = qtile self.testXeyes() # self.testWindow("one") assert self.c.window.info()['width'] == 798 assert self.c.window.info()['height'] == 578 assert self.c.window.info()['x'] == 0 assert self.c.window.info()['y'] == 0 self.c.window.toggle_floating() assert self.c.window.info()['floating'] is True self.c.window.move_floating(10, 20, 42, 42) assert self.c.window.info()['width'] == 150 assert self.c.window.info()['height'] == 100 assert self.c.window.info()['x'] == 10 assert self.c.window.info()['y'] == 20 self.c.window.set_size_floating(50, 90, 42, 42) assert self.c.window.info()['width'] == 50 assert self.c.window.info()['height'] == 90 assert self.c.window.info()['x'] == 10 assert self.c.window.info()['y'] == 20 self.c.window.resize_floating(10, 20, 42, 42) assert self.c.window.info()['width'] == 60 assert self.c.window.info()['height'] == 110 assert self.c.window.info()['x'] == 10 assert self.c.window.info()['y'] == 20 self.c.window.set_size_floating(10, 20, 42, 42) assert self.c.window.info()['width'] == 10 assert self.c.window.info()['height'] == 20 assert self.c.window.info()['x'] == 10 assert self.c.window.info()['y'] == 20 # change layout (x, y should be same) self.c.next_layout() assert self.c.window.info()['width'] == 10 assert self.c.window.info()['height'] == 20 assert self.c.window.info()['x'] == 10 assert self.c.window.info()['y'] == 20 @manager_config @no_xinerama def test_screens(qtile): self = qtile assert len(self.c.screens()) @manager_config @no_xinerama def test_rotate(qtile): self = qtile self.testWindow("one") s = self.c.screens()[0] height, width = s["height"], s["width"] subprocess.call( [ "xrandr", "--output", "default", "-display", self.display, "--rotate", "left" ], stderr=subprocess.PIPE, stdout=subprocess.PIPE ) for _ in range(10): time.sleep(0.1) s = self.c.screens()[0] if s["width"] == height and s["height"] == width: break else: raise AssertionError("Screen did not rotate") # TODO: see note on test_resize @manager_config @no_xinerama def test_resize_(qtile): self = qtile self.testWindow("one") subprocess.call( [ "xrandr", "-s", "480x640", "-display", self.display ] ) for _ in range(10): time.sleep(0.1) d = self.c.screen.info() if d["width"] == 480 and d["height"] == 640: break else: raise AssertionError("Screen did not resize") @manager_config @no_xinerama def test_focus_stays_on_layout_switch(qtile): qtile.testWindow("one") qtile.testWindow("two") # switch to a double stack layout qtile.c.next_layout() # focus on a different window than the default qtile.c.layout.next() # toggle the layout qtile.c.next_layout() qtile.c.prev_layout() assert qtile.c.window.info()['name'] == 'one' @pytest.mark.parametrize("qtile", [BareConfig, ManagerConfig], indirect=True) @pytest.mark.parametrize("xephyr", [{"xinerama": True}, {"xinerama": False}], indirect=True) def test_xeyes(qtile): qtile.testXeyes() @pytest.mark.parametrize("qtile", [BareConfig, ManagerConfig], indirect=True) @pytest.mark.parametrize("xephyr", [{"xinerama": True}, {"xinerama": False}], indirect=True) def test_xterm(qtile): qtile.testXterm() @pytest.mark.parametrize("qtile", [BareConfig, ManagerConfig], indirect=True) @pytest.mark.parametrize("xephyr", [{"xinerama": True}, {"xinerama": False}], indirect=True) def test_xterm_kill_window(qtile): self = qtile self.testXterm() self.c.window.kill() self.c.sync() for _ in range(10): time.sleep(0.1) if not self.c.windows(): break else: raise AssertionError("xterm did not die") @pytest.mark.parametrize("qtile", [BareConfig, ManagerConfig], indirect=True) @pytest.mark.parametrize("xephyr", [{"xinerama": True}, {"xinerama": False}], indirect=True) def test_map_request(qtile): self = qtile self.testWindow("one") info = self.c.groups()["a"] assert "one" in info["windows"] assert info["focus"] == "one" self.testWindow("two") info = self.c.groups()["a"] assert "two" in info["windows"] assert info["focus"] == "two" @pytest.mark.parametrize("qtile", [BareConfig, ManagerConfig], indirect=True) @pytest.mark.parametrize("xephyr", [{"xinerama": True}, {"xinerama": False}], indirect=True) def test_unmap(qtile): self = qtile one = self.testWindow("one") two = self.testWindow("two") three = self.testWindow("three") info = self.c.groups()["a"] assert info["focus"] == "three" assert len(self.c.windows()) == 3 self.kill_window(three) assert len(self.c.windows()) == 2 info = self.c.groups()["a"] assert info["focus"] == "two" self.kill_window(two) assert len(self.c.windows()) == 1 info = self.c.groups()["a"] assert info["focus"] == "one" self.kill_window(one) assert len(self.c.windows()) == 0 info = self.c.groups()["a"] assert info["focus"] is None @pytest.mark.parametrize("qtile", [BareConfig, ManagerConfig], indirect=True) @pytest.mark.parametrize("xephyr", [{"xinerama": True}, {"xinerama": False}], indirect=True) def test_setgroup(qtile): self = qtile self.testWindow("one") self.c.group["b"].toscreen() self.groupconsistency() if len(self.c.screens()) == 1: assert self.c.groups()["a"]["screen"] is None else: assert self.c.groups()["a"]["screen"] == 1 assert self.c.groups()["b"]["screen"] == 0 self.c.group["c"].toscreen() self.groupconsistency() assert self.c.groups()["c"]["screen"] == 0 @pytest.mark.parametrize("qtile", [BareConfig, ManagerConfig], indirect=True) @pytest.mark.parametrize("xephyr", [{"xinerama": True}, {"xinerama": False}], indirect=True) def test_unmap_noscreen(qtile): self = qtile self.testWindow("one") pid = self.testWindow("two") assert len(self.c.windows()) == 2 self.c.group["c"].toscreen() self.groupconsistency() self.c.status() assert len(self.c.windows()) == 2 self.kill_window(pid) assert len(self.c.windows()) == 1 assert self.c.groups()["a"]["focus"] == "one" def test_init(): with pytest.raises(libqtile.manager.QtileError): libqtile.config.Key([], "unknown", libqtile.command._Call("base", None, "foo")) with pytest.raises(libqtile.manager.QtileError): libqtile.config.Key(["unknown"], "x", libqtile.command._Call("base", None, "foo")) class TScreen(libqtile.config.Screen): def setGroup(self, x, save_prev=True): pass def test_dx(): s = TScreen(left=libqtile.bar.Gap(10)) s._configure(None, 0, 0, 0, 100, 100, None) assert s.dx == 10 def test_dwidth(): s = TScreen(left=libqtile.bar.Gap(10)) s._configure(None, 0, 0, 0, 100, 100, None) assert s.dwidth == 90 s.right = libqtile.bar.Gap(10) assert s.dwidth == 80 def test_dy(): s = TScreen(top=libqtile.bar.Gap(10)) s._configure(None, 0, 0, 0, 100, 100, None) assert s.dy == 10 def test_dheight(): s = TScreen(top=libqtile.bar.Gap(10)) s._configure(None, 0, 0, 0, 100, 100, None) assert s.dheight == 90 s.bottom = libqtile.bar.Gap(10) assert s.dheight == 80 class _Config(object): groups = [ libqtile.config.Group("a"), libqtile.config.Group("b"), libqtile.config.Group("c"), libqtile.config.Group("d") ] layouts = [ libqtile.layout.stack.Stack(num_stacks=1), libqtile.layout.stack.Stack(num_stacks=2) ] floating_layout = libqtile.layout.floating.Floating() keys = [ libqtile.config.Key( ["control"], "k", libqtile.command._Call([("layout", None)], "up") ), libqtile.config.Key( ["control"], "j", libqtile.command._Call([("layout", None)], "down") ), ] mouse = [] screens = [libqtile.config.Screen( bottom=libqtile.bar.Bar( [ libqtile.widget.GroupBox(), ], 20 ), )] auto_fullscreen = True class ClientNewStaticConfig(_Config): @staticmethod def main(c): def client_new(c): c.static(0) libqtile.hook.subscribe.client_new(client_new) clientnew_config = pytest.mark.parametrize("qtile", [ClientNewStaticConfig], indirect=True) @clientnew_config def test_minimal_(qtile): self = qtile a = self.testWindow("one") self.kill_window(a) @pytest.mark.skipif(whereis("gkrellm") is None, reason="gkrellm not found") @clientnew_config def test_gkrellm(qtile): qtile.testGkrellm() time.sleep(0.1) class ToGroupConfig(_Config): @staticmethod def main(c): def client_new(c): c.togroup("d") libqtile.hook.subscribe.client_new(client_new) togroup_config = pytest.mark.parametrize("qtile", [ToGroupConfig], indirect=True) @togroup_config def test_minimal__(qtile): qtile.c.group["d"].toscreen() qtile.c.group["a"].toscreen() a = qtile.testWindow("one") assert len(qtile.c.group["d"].info()["windows"]) == 1 qtile.kill_window(a) @manager_config def test_colorPixel(qtile): # test for #394 qtile.c.eval("self.colorPixel(\"ffffff\")") qtile-0.10.7/test/test_sh.py000066400000000000000000000077261305063162100157300ustar00rootroot00000000000000# Copyright (c) 2011 Florian Mounier # Copyright (c) 2012 Tycho Andersen # Copyright (c) 2014 Sean Vig # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import pytest import libqtile import libqtile.sh import libqtile.confreader import libqtile.layout import libqtile.manager import libqtile.config class ShConfig(object): keys = [] mouse = [] groups = [ libqtile.config.Group("a"), libqtile.config.Group("b"), ] layouts = [ libqtile.layout.Max(), ] floating_layout = libqtile.layout.floating.Floating() screens = [ libqtile.config.Screen() ] main = None sh_config = pytest.mark.parametrize("qtile", [ShConfig], indirect=True) @sh_config def test_columnize(qtile): qtile.sh = libqtile.sh.QSh(qtile.c) assert qtile.sh.columnize(["one", "two"]) == "one two" qtile.sh.termwidth = 1 assert qtile.sh.columnize(["one", "two"], update_termwidth=False) == "one\ntwo" qtile.sh.termwidth = 15 v = qtile.sh.columnize(["one", "two", "three", "four", "five"], update_termwidth=False) assert v == 'one two \nthree four \nfive ' @sh_config def test_ls(qtile): qtile.sh = libqtile.sh.QSh(qtile.c) qtile.sh.do_cd("layout") qtile.sh.do_ls("") @sh_config def test_findNode(qtile): qtile.sh = libqtile.sh.QSh(qtile.c) n = qtile.sh._findNode(qtile.sh.current, "layout") assert n.path == "layout" assert n.parent n = qtile.sh._findNode(n, "0") assert n.path == "layout[0]" n = qtile.sh._findNode(n, "..") assert n.path == "layout" n = qtile.sh._findNode(n, "0", "..") assert n.path == "layout" n = qtile.sh._findNode(n, "..", "layout", 0) assert n.path == "layout[0]" assert not qtile.sh._findNode(n, "wibble") assert not qtile.sh._findNode(n, "..", "0", "wibble") @sh_config def test_do_cd(qtile): qtile.sh = libqtile.sh.QSh(qtile.c) assert qtile.sh.do_cd("layout") == 'layout' assert qtile.sh.do_cd("0/wibble") == 'No such path.' assert qtile.sh.do_cd("0/") == 'layout[0]' @sh_config def test_call(qtile): qtile.sh = libqtile.sh.QSh(qtile.c) assert qtile.sh._call("status", []) == "OK" v = qtile.sh._call("nonexistent", "") assert "No such command" in v v = qtile.sh._call("status", "(((") assert "Syntax error" in v v = qtile.sh._call("status", "(1)") assert "Command exception" in v @sh_config def test_complete(qtile): qtile.sh = libqtile.sh.QSh(qtile.c) assert qtile.sh._complete("c", "c") == [ "cd", "commands", "critical", ] assert qtile.sh._complete("cd l", "l") == ["layout"] assert qtile.sh._complete("cd layout/", "layout/") == [ "layout/" + x for x in ["group", "window", "screen", "0"] ] assert qtile.sh._complete("cd layout/", "layout/g") == ["layout/group"] @sh_config def test_help(qtile): qtile.sh = libqtile.sh.QSh(qtile.c) assert qtile.sh.do_help("nonexistent").startswith("No such command") assert qtile.sh.do_help("help") qtile-0.10.7/test/test_utils.py000066400000000000000000000056751305063162100164570ustar00rootroot00000000000000# Copyright (c) 2008, 2010 Aldo Cortesi # Copyright (c) 2011 Florian Mounier # Copyright (c) 2011 Anshuman Bhaduri # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import sys import six import libqtile.utils as utils class Foo(object): ran = False @utils.lru_cache(2) def one(self, x): self.ran = True return x def test_translate_masks(): assert utils.translate_masks(["shift", "control"]) assert utils.translate_masks([]) == 0 def test_lrucache_works_as_decorator(): f = Foo() assert f.one(1) == 1 assert f.one('test') == 'test' def test_lrucache_caches(): f = Foo() f.one(1) f.one(2) f.ran = False f.one(1) assert not f.ran f.one(2) assert not f.ran def test_lrucache_discards_lru_item(): f = Foo() f.one(1) assert f.ran f.ran = False f.one(1) assert not f.ran f.one(2) f.one(3) f.one(1) assert f.ran def test_lrucache_maintains_size(): f = Foo() f.one(1) f.one(2) f.one(3) # we only need these checks for the homebuilt LRU cache if sys.version_info < (3, 3): assert len(f._cached_one) == 2 assert len(f._cachelist_one) == 2 def test_rgb_from_hex_number(): assert utils.rgb("ff00ff") == (1, 0, 1, 1) def test_rgb_from_hex_string(): assert utils.rgb("#00ff00") == (0, 1, 0, 1) def test_rgb_from_hex_number_with_alpha(): assert utils.rgb("ff0000.3") == (1, 0, 0, 0.3) def test_rgb_from_hex_string_with_alpha(): assert utils.rgb("#ff0000.5") == (1, 0, 0, 0.5) def test_rgb_from_base10_tuple(): assert utils.rgb([255, 255, 0]) == (1, 1, 0, 1) def test_rgb_from_base10_tuple_with_alpha(): assert utils.rgb([255, 255, 0, 0.5]) == (1, 1, 0, 0.5) def test_scrub_to_utf8(): assert utils.scrub_to_utf8(six.b("foo")) == six.u("foo") def test_shuffle(): l = list(range(3)) utils.shuffleUp(l) assert l != list(range(3)) utils.shuffleDown(l) assert l == list(range(3)) qtile-0.10.7/test/test_widget.py000066400000000000000000000056201305063162100165700ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2015 Tycho Andersen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Widget specific tests import pytest from libqtile.config import Screen from libqtile.bar import Bar from libqtile.widget import TextBox, ThermalSensor from .conftest import BareConfig class ColorChanger(TextBox): count = 0 def update(self, text): self.count += 1 if self.count % 2 == 0: self.foreground = "ff0000" else: self.foreground = "0000ff" self.text = text class WidgetTestConf(BareConfig): screens = [Screen(bottom=Bar([ColorChanger(name="colorchanger")], 20))] widget_conf = pytest.mark.parametrize("qtile", [WidgetTestConf], indirect=True) @widget_conf def test_textbox_color_change(qtile): qtile.c.widget["colorchanger"].update('f') assert qtile.c.widget["colorchanger"].info()["foreground"] == "0000ff" qtile.c.widget["colorchanger"].update('f') assert qtile.c.widget["colorchanger"].info()["foreground"] == "ff0000" def test_thermalsensor_regex_compatibility(): sensors = ThermalSensor() test_sensors_output = """ coretemp-isa-0000 Adapter: ISA adapter Physical id 0: +61.0°C (high = +86.0°C, crit = +100.0°C) Core 0: +54.0°C (high = +86.0°C, crit = +100.0°C) Core 1: +56.0°C (high = +86.0°C, crit = +100.0°C) Core 2: +58.0°C (high = +86.0°C, crit = +100.0°C) Core 3: +61.0°C (high = +86.0°C, crit = +100.0°C) """ sensors_detected = sensors._format_sensors_output(test_sensors_output) assert sensors_detected["Physical id 0"] == ("61.0", "°C") assert sensors_detected["Core 0"] == ("54.0", "°C") assert sensors_detected["Core 1"] == ("56.0", "°C") assert sensors_detected["Core 2"] == ("58.0", "°C") assert sensors_detected["Core 3"] == ("61.0", "°C") assert not ("Adapter" in sensors_detected.keys()) qtile-0.10.7/tox.ini000066400000000000000000000026741305063162100142360ustar00rootroot00000000000000[tox] skip_missing_interpreters = True skipsdist=True minversion = 1.8 envlist = py27-trollius, py33-trollius, py33-tulip, py34, py35, py36, pypy-trollius, pypy3-trollius, py-nightly docs, pep8, packaging [testenv] # This is required in order to get UTF-8 output inside of the subprocesses # that our tests use. setenv = LC_CTYPE = en_US.UTF-8 # Pass Display down to have it for the tests available passenv = DISPLAY TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH whitelist_externals = /bin/bash # Install trollius for 2 and pypy # Install either trollius or tulip for 3.3 # Asyncio is in the standard library for 3.4+ deps = pytest-cov trollius: trollius tulip: asyncio commands = # xcffib has to be installed before cairo pip install xcffib # Install unpinned requirements from requirements.in pip install -r {toxinidir}/requirements.in # build pangocffi module bash -c "python -c 'import cffi, sys; sys.exit(cffi.__version_info__[0])' || python {toxinidir}/libqtile/ffi_build.py" py.test --cov libqtile --cov-report term-missing [testenv:packaging] deps = check-manifest readme_renderer commands = check-manifest python setup.py check -m -r -s [testenv:pep8] deps = flake8 commands = flake8 {toxinidir}/libqtile {toxinidir}/bin/ [testenv:docs] deps = -r{toxinidir}/docs/requirements.txt commands = sphinx-build -W -b html -d {envtmpdir}/doctrees docs docs/_build/html