Merge branch 'master' of https://github.com/qtile/qtile into qtile-master

master
Max Cohen 2021-12-23 15:48:59 +01:00
commit f6c9637651
221 changed files with 7132 additions and 1594 deletions

4
.github/FUNDING.yml vendored
View File

@ -1,2 +1,2 @@
github: [ramnes]
custom: ["https://paypal.me/ramnes"]
github: ["m-col"]
custom: ["https://liberapay.com/mcol"]

View File

@ -1,33 +0,0 @@
<!--
Please do not ask general questions here! There are [community
contact](https://github.com/qtile/qtile#community) options for that.
-->
# Issue description
<!--
A brief discussion of what failed and how it failed. A description of
what you tried is helpful, i.e. "When I use lazy.kill() on a window I get
the following stack trace" instead of "Closing windows doesn't work".
-->
# Qtile version
<!--
Please include the exact commit hash of the version of Qtile that failed.
-->
# Stack traces
<!--
Please attach any stack traces found in:
* `~/.xsession-errors`
* `~/.local/share/qtile/qtile.log`
-->
# Configuration
<!--
Please include a link or attach your configuration to the issue.
-->

32
.github/ISSUE_TEMPLATE/bug_report.yaml vendored Normal file
View File

@ -0,0 +1,32 @@
name: "Issue template"
description: Required
labels: ["unconfirmed"]
body:
- type: markdown
attributes:
value: |
Please note that this tracker is only for bugs or other issues with the core project.
Have a **general question**?
There are [community contact](https://github.com/qtile/qtile#community) options for that, or ask at [Q&A](https://github.com/qtile/qtile/discussions/categories/q-a).
Have a **feature idea**?
Please post it on the discussions board as an [idea](https://github.com/qtile/qtile/discussions/categories/ideas).
- type: markdown
attributes:
value: |
Please include:
- Your Qtile version (`qtile --version`).
- Relevant **logs** from `~/.local/share/qtile/qtile.log`.
- If relevant, the problematic part of your config.
- type: textarea
attributes:
label: "The bug:"
value: I wanted to do X, but Y happened, and I expected Z. I think this is a bug.
validations:
render: markdown
- type: checkboxes
attributes:
options:
- label: I have searched past issues to see if this bug has already been reported.
required: true

View File

@ -18,7 +18,7 @@ jobs:
matrix:
# if you change one of these, be sure to update
# /tox.ini:[gh-actions] as well
python-version: [pypy-3.7, 3.7, 3.8, 3.9]
python-version: [pypy-3.7, 3.7, 3.8, 3.9, '3.10']
steps:
- uses: actions/checkout@v2
- name: Set up python ${{ matrix.python-version }}
@ -30,10 +30,11 @@ jobs:
sudo apt update
sudo apt install --no-install-recommends \
libdbus-1-dev libgirepository1.0-dev gir1.2-gtk-3.0 gir1.2-notify-0.7 gir1.2-gudev-1.0 graphviz \
imagemagick libpulse-dev lm-sensors git xserver-xephyr xterm xvfb ninja-build libegl1-mesa-dev \
imagemagick libpulse-dev git xserver-xephyr xterm xvfb ninja-build libegl1-mesa-dev \
libgles2-mesa-dev libgbm-dev libinput-dev libxkbcommon-dev libpixman-1-dev libpciaccess-dev \
dbus-x11 libnotify-bin
sudo pip -q install tox tox-gh-actions meson PyGObject
sudo pip -q install meson PyGObject
pip -q install "tox<4" tox-gh-actions
- name: Build wayland
run: |
wget -q --no-check-certificate https://wayland.freedesktop.org/releases/wayland-$WAYLAND.tar.xz
@ -74,8 +75,26 @@ jobs:
meson build -Dexamples=false --prefix=/usr
ninja -C build
sudo ninja -C build install
- name: run tests
- name: Run Tests
run: |
[ "$(grep -c -P '\t' CHANGELOG)" = "0" ]
tox
- name: Upload coverage data to coveralls.io
run: |
pip -q install coveralls
coveralls --service=github
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_FLAG_NAME: ${{ matrix.python-version }}
COVERALLS_PARALLEL: true
coverage:
name: Finalize Coverage
needs: build
runs-on: ubuntu-20.04
steps:
- name: Coveralls Finished
uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
parallel-finished: true

View File

@ -1,4 +1,37 @@
Qtile xxxxxx, released xxxxxxxxxx:
Qtile x.x.x, released xxxx-xx-xx:
* features
- Add `place_right` option in the TreeTab layout to place the tab panel on the right side
Qtile 0.19.0, released 2021-12-22:
* features
- Add ability to draw borders to the Bar. Can customise size and colour per edge.
- Add `StatusNotifier` widget implementing the `StatusNotifierItem` specification.
NB Widget does not provide context menus.
- Add `total` bandwidth format value to the Net widget.
- Scratchpad groups could be defined as single so that only one of the scratchpad in the group is visible
at a given time.
- All scratchpads in a Scratchpad group can be hidden with hide_all() function.
- For saving states of scratchpads during restart, we use wids instead of pids.
- Scratchpads can now be defined with an optional matcher to match with window properties.
- `Qtile.cmd_reload_config` is added for reloading the config without completely restarting.
- Window.cmd_togroup's argument `groupName` should be changed to
`group_name`. For the time being a log warning is in place and a
migration is added. In the future `groupName` will fail.
- Add `min/max_ratio` to Tile layout and fix bug where windows can extend offscreen.
- Add ability for widget `mouse_callbacks` to take `lazy` calls (similar to keybindings)
- Add `aliases` to `lazy.spawncmd()` which takes a dictionary mapping convenient aliases
to full command lines.
- Add a new 'prefix' option to the net widget to display speeds with a static unit (e.g. MB).
- `lazy.group.toscreen()` now does not toggle groups by default. To get this behaviour back, use
`lazy.group.toscreen(toggle=True)`
- Tile layout has new `margin_on_single` and `border_on_single` option to specify
whether to draw margin and border when there is only one window.
- Thermal zone widget.
- Allow TextBox-based widgets to display in vertical bars.
- Added a focused attribute to `lazy.function.when` which can be used to Match on focused windows.
- Allow to update Image widget with update() function by giving a new path.
Qtile 0.18.1, released 2021-09-16:
* features
- All layouts will accept a list of colors for border_* options with which
they will draw multiple borders on the appropriate windows.

View File

@ -1,51 +1,5 @@
# 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](https://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](https://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: conforms to PEP8 and passes `make lint`.
2. Unit tests: CI tests pass. Adding new tests to verify that your code works is recommended.
See [our website](http://docs.qtile.org/en/latest/manual/contributing.html#running-tests-locally)
on how to run the tests locally.
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
Instead of making this document a copy of [the _contributing_ section of our
documentation](https://docs.qtile.org/en/latest/manual/contributing.html),
we just link to it here.

View File

@ -24,7 +24,7 @@ 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
:Mailing List: https://groups.google.com/group/qtile-dev
:IRC: irc://irc.oftc.net:6667/qtile
Contributing
@ -35,15 +35,15 @@ 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
.. _`tips & tricks`: https://docs.qtile.org/en/latest/manual/hacking.html
.. _`guidelines`: https://docs.qtile.org/en/latest/manual/contributing.html
.. |logo| image:: https://raw.githubusercontent.com/qtile/qtile/master/logo.png
:alt: Logo
:target: http://www.qtile.org
:target: https://www.qtile.org
.. |website| image:: https://img.shields.io/badge/website-qtile.org-blue.svg
:alt: Website
:target: http://www.qtile.org
:target: https://www.qtile.org
.. |pypi| image:: https://img.shields.io/pypi/v/qtile.svg
:alt: PyPI
:target: https://pypi.org/project/qtile/
@ -52,7 +52,7 @@ and `guidelines`_ for contributing in the documentation.
:target: https://github.com/qtile/qtile/actions
.. |rtd| image:: https://readthedocs.org/projects/qtile/badge/?version=latest
:alt: Read the Docs
:target: http://docs.qtile.org/en/latest/
:target: https://docs.qtile.org/en/latest/
.. |license| image:: https://img.shields.io/github/license/qtile/qtile.svg
:alt: License
:target: https://github.com/qtile/qtile/blob/master/LICENSE

View File

@ -24,32 +24,27 @@ digraph G {
node [style="filled", color=Purple, fillcolor=Violet, label="window"];
window;
node [style="filled", color=SlateBlue, fillcolor=SlateBlue1, label="core"];
core;
root -> bar;
root -> group;
root -> layout;
root -> screen;
root -> widget;
root -> window;
root -> core;
bar -> screen;
bar -> screen [dir=both];
bar -> widget [dir=both];
group -> layout;
group -> screen;
group -> window;
group -> layout [dir=both];
group -> screen [dir=both];
group -> window [dir=both];
layout -> group;
layout -> screen;
layout -> window;
layout -> screen [dir=both];
layout -> window [dir=both];
screen -> bar;
screen -> layout;
screen -> window;
widget -> bar;
widget -> group;
widget -> screen;
window -> group;
window -> screen;
window -> layout;
screen -> window [dir=both];
screen -> widget [dir=both];
}

View File

@ -27,6 +27,7 @@ class Mock(MagicMock):
MOCK_MODULES = [
'libqtile._ffi_pango',
'libqtile.backend.x11._ffi_xcursors',
'libqtile.widget._pulse_audio',
'cairocffi',
'cairocffi.xcb',
'cairocffi.pixbuf',
@ -35,13 +36,33 @@ MOCK_MODULES = [
'dateutil.parser',
'dbus_next',
'dbus_next.aio',
'dbus_next.errors',
'dbus_next.service',
'dbus_next.constants',
'iwlib',
'keyring',
'mpd',
'psutil',
'trollius',
'pywayland',
'pywayland.protocol.wayland',
'pywayland.server',
'wlroots',
'wlroots.helper',
'wlroots.util',
'wlroots.util.box',
'wlroots.util.clock',
'wlroots.util.edges',
'wlroots.util.region',
'wlroots.wlr_types',
'wlroots.wlr_types.cursor',
'wlroots.wlr_types.foreign_toplevel_management_v1',
'wlroots.wlr_types.keyboard',
'wlroots.wlr_types.layer_shell_v1',
'wlroots.wlr_types.output_management_v1',
'wlroots.wlr_types.pointer_constraints_v1',
'wlroots.wlr_types.server_decoration',
'wlroots.wlr_types.virtual_keyboard_v1',
'wlroots.wlr_types.xdg_shell',
'xcffib',
'xcffib.randr',
'xcffib.render',
@ -50,6 +71,7 @@ MOCK_MODULES = [
'xcffib.xinerama',
'xcffib.xproto',
'xdg.IconTheme',
'xkbcommon'
]
sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES)
@ -94,7 +116,7 @@ master_doc = 'index'
# General information about the project.
project = u'Qtile'
copyright = u'2008-2020, Aldo Cortesi and contributers'
copyright = u'2008-2021, 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
@ -186,6 +208,10 @@ html_static_path = ['_static']
# typographically correct entities.
#html_use_smartypants = True
# smartypants was deprecated in favour of smartquotes
# We want to disable this so users can copy an paste text into their configs
smartquotes = False
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}

View File

@ -11,20 +11,20 @@ Getting started
manual/install/index
manual/config/index
manual/troubleshooting
As a Wayland Compositor <manual/wayland>
manual/commands/shell/index
- :ref:`ref-extensions`
- :ref:`ref-hooks`
- :ref:`ref-layouts`
- :ref:`ref-widgets`
Reference
=========
.. toctree::
:maxdepth: 2
:hidden:
manual/ref/index
:maxdepth: 1
manual/ref/extensions
manual/ref/hooks
manual/ref/layouts
manual/ref/widgets
manual/config/default
Advanced scripting
==================
@ -43,8 +43,8 @@ Getting involved
.. toctree::
:maxdepth: 1
manual/contributing
manual/hacking
manual/contributing
Miscellaneous
=============
@ -62,5 +62,6 @@ Tips & Tricks
:maxdepth: 1
manual/howto/widget
manual/howto/git
* :ref:`genindex`

View File

@ -10,9 +10,9 @@ different places:
* Commands can be :ref:`bound to keys <config-keys>` in the Qtile
configuration file.
* Commands can be :ref:`called through qtile shell <qshell>`, the
* Commands can be :ref:`called through qtile shell <qtile-shell>`, the
Qtile shell.
* The qsh can also be hooked into a Jupyter kernel :ref:`called iqshell
* The shell can also be hooked into a Jupyter kernel :ref:`called iqshell
<iqshell>`.
* Commands can be :ref:`called from a script <scripting>` to
interact with Qtile from Python.
@ -121,6 +121,8 @@ specifies the group belonging to the screen that belongs to group "b":
This amout of connectivity makes it easy to reach out from a given object when
callbacks and events fire on that object to related objects.
.. _object_graph_keys:
Keys
====
@ -212,7 +214,7 @@ Any call can be resolved from a given node. In addition, each node knows about
all of the children objects that can be reached from it and have the ability to
``.navigate()`` to the other nodes in the command graph. Each of the object
types are represented as ``CommandGraphObject`` types and the root node of the
graph, the ``CommandGraphRoot`` reresents the Qtile instance. When a call is
graph, the ``CommandGraphRoot`` represents the Qtile instance. When a call is
performed on an object, it returns a ``CommandGraphCall``. Each call will know
its own name as well as be able to resolve the path through the command graph
to be able to find itself.
@ -273,7 +275,7 @@ command graph, and traversal is done by creating a new command client starting
from the new node. When a command is executed against a node, that command is
dispatched to the held command interface. The key decision here is how to
perform the traversal. The command client exists in two different flavors: the
standard ``ComandClient`` which is useful for handling more programatic
standard ``CommandClient`` which is useful for handling more programatic
traversal of the graph, calling methods to traverse the graph, and the
``InteractiveCommandClient`` which behaves more like a standard Python object,
traversing by accessing properties and performing key lookups.

View File

@ -13,12 +13,12 @@ This allows Qtile to be controlled fully from external scripts. Remote
interaction occurs through an instance of the
``libqtile.command.interface.IPCCommandInterface`` class. This class
establishes a connection to the currently running instance of Qtile. A
``libqtile.command.client.CommandClient`` can use this connection to dispatch
``libqtile.command.client.InteractiveCommandClient`` can use this connection to dispatch
commands to the running instance. Commands then appear as methods with the
appropriate signature on the ``CommandClient`` object. The object hierarchy is
appropriate signature on the ``InteractiveCommandClient`` object. The object hierarchy is
described in the :ref:`commands-api` section of this manual. Full
command documentation is available through the :ref:`Qtile Shell
<qshell>`.
<qtile-shell>`.
Example
@ -29,6 +29,6 @@ instance, and returns the integer offset of the current screen.
.. code-block:: python
from libqtile.command.client import CommandClient
c = CommandClient()
from libqtile.command.client import InteractiveCommandClient
c = InteractiveCommandClient()
print(c.screen.info()["index"])

View File

@ -10,7 +10,7 @@ repository are also documented below.
:maxdepth: 1
qtile start <qtile-start>
qtile shell <qshell>
qtile shell <qtile-shell>
qtile cmd-obj <qtile-cmd>
qtile run-cmd <qtile-run>
qtile top <qtile-top>

View File

@ -4,6 +4,51 @@ qtile cmd-obj
This is a simple tool to expose qtile.command functionality to shell.
This can be used standalone or in other shell scripts.
How it works
------------
``qtile cmd-obj`` works by selecting a command object and calling a specified function of that object.
As per :ref:`commands-api`, Qtile's object graph has seven nodes: ``layout``, ``window``, ``group``,
``bar``, ``widget``, ``screen``, and a special ``root`` node. These are the objects that can be accessed
via ``qtile cmd-obj`` (NB the root node is called ``cmd`` when using the ``cmd-obj`` script to give it
an addressable name).
Running the command against a selected object without a function (``-f``) will run the ``help``
command and list the commands available to the object. Commands shown with an asterisk ("*") require
arguments to be passed via the ``-a`` flag.
Selecting an object
~~~~~~~~~~~~~~~~~~~
With the exception of ``cmd``, all objects need an identifier so the correct object can be selected. Refer to
:ref:`object_graph_keys` for more information.
.. note::
You will see from the graph on :ref:`commands-api` that certain objects can be accessed from other objects.
For example, ``qtile cmd-obj -o group term layout`` will list the commands for the current layout on the
``term`` group.
Information on functions
~~~~~~~~~~~~~~~~~~~~~~~~
Running a function with the ``-i`` flag will provide additional detail about that function (i.e. what it does and what
arguments it expects).
Passing arguments to functions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Arguments can be passed to a function by using the ``-a`` flag. For example, to change the label for the group named "1"
to "A", you would run ``qtile cmd-obj -o group 1 -f set_label -a A``.
.. warning::
It is not currently possible to pass non-string arguments to functions via ``qtile cmd-obj``. Doing so will
result in an error.
Examples:
---------
@ -35,6 +80,8 @@ Output of ``qtile cmd-obj -h``
qtile cmd-obj -o cmd -f prev_layout -i
qtile cmd-obj -o cmd -f prev_layout -a 3 # prev_layout on group 3
qtile cmd-obj -o group 3 -f focus_back
qtile cmd-obj -o widget textbox -f update -a "New text"
qtile cmd-obj -o cmd -f restart # restart qtile
Output of ``qtile cmd-obj -o group 3``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -96,7 +143,6 @@ Output of ``qtile cmd-obj -o cmd``
-o cmd -f remove_rule * Remove a dgroup rule by rule_id
-o cmd -f restart Restart qtile
-o cmd -f run_extension * Run extensions
-o cmd -f run_extention * Deprecated alias for cmd_run_extension()
-o cmd -f run_external * Run external Python script
-o cmd -f screens Return a list of dictionaries providing information on all screens
-o cmd -f shutdown Quit Qtile

View File

@ -1,4 +1,4 @@
.. _qshell:
.. _qtile-shell:
===========
qtile shell
@ -23,21 +23,54 @@ builtin "cd" and "ls" commands act like their familiar shell counterparts:
> ls
layout/ widget/ screen/ bar/ window/ group/
> cd bar
> cd screen
layout/ window/ bar/ widget/
bar> ls
bottom/
bar> cd bottom
bar['bottom']> ls
screen/
bar['bottom']> cd ../..
> cd ..
/
> ls
layout/ widget/ screen/ bar/ window/ group/
If you try to access an object that has no "default" value then you will see an
error message:
.. code-block:: bash
> ls
layout/ widget/ screen/ bar/ window/ group/
> cd bar
Item required for bar
> ls bar
bar[bottom]/
> cd bar/bottom
bar['bottom']> ls
screen/ widget/
Please refer to :ref:`object_graph_keys` for a summary of which objects need a
specified selector and the type of selector required. Using ``ls`` will show
which selectors are available for an object. Please see below for an explanation
about how Qtile displays shell paths.
Alternatively, the ``items()`` command can be run on the parent object to show which
selectors are available. The first value shows whether a selector is optional
(``False`` means that a selector is required) and the second value is a list of
selectors:
.. code-block:: bash
> ls
layout/ widget/ screen/ bar/ window/ group/
> items(bar)
(False, ['bottom'])
Displaying the shell path
=========================
Note that the shell provides a "short-hand" for specifying node keys (as
opposed to children). The following is a valid shell path:

View File

@ -2,4 +2,16 @@
qtile top
=========
Is a top like to measure memory usage of Qtile's internals.
``qtile top`` is a ``top``-like tool to measure memory usage of Qtile's internals.
.. note::
To use ``qtile shell`` you need to have ``tracemalloc`` enabled. You can do this by
setting the environmental variable ``PYTHONTRACEMALLOC=1`` before starting qtile.
Alternatively, you can force start ``tracemalloc`` but you will lose early traces:
.. code-block::
>>> from libqtile.command.client import InteractiveCommandClient
>>> i=InteractiveCommandClient()
>>> i.eval("import tracemalloc;tracemalloc.start()")

View File

@ -0,0 +1,7 @@
.. _default_config:
===================
Default Config File
===================
.. literalinclude:: ../../../libqtile/resources/default_config.py

View File

@ -58,7 +58,7 @@ bind thats process' window to it. The associated window can then be shown and
hidden by the lazy command ``dropdown_toggle()``
(see :ref:`lazy`) from the ScratchPad group.
Thus - for example - your favorite terminal emulator turns into a quake-like
terminal by the control of qtile.
terminal by the control of Qtile.
If the DropDown window turns visible it is placed as a floating window on top
of the current group.
@ -77,8 +77,8 @@ Example
# it is placed in the upper third of screen by default.
DropDown("term", "urxvt", opacity=0.8),
# define another terminal exclusively for qshell at different position
DropDown("qshell", "urxvt -hold -e qshell",
# define another terminal exclusively for ``qtile shell` at different position
DropDown("qtile shell", "urxvt -hold -e 'qtile shell'",
x=0.05, y=0.4, width=0.9, height=0.6, opacity=0.9,
on_focus_lost_hide=True) ]),
Group("a"),
@ -87,21 +87,22 @@ Example
keys = [
# toggle visibiliy of above defined DropDown named "term"
Key([], 'F11', lazy.group['scratchpad'].dropdown_toggle('term')),
Key([], 'F12', lazy.group['scratchpad'].dropdown_toggle('qshell')),
Key([], 'F12', lazy.group['scratchpad'].dropdown_toggle('qtile shell')),
]
There is only one DropDown visible in current group at a time.
If a further DropDown is set visible the currently shown DropDown turns
invisble immediately.
Note that if the window is set to not floating, it is detached from DropDown
and ScratchPad, and a new pocess is spawned next time the DropDown is set visible.
and ScratchPad, and a new process is spawned next time the DropDown is set
visible.
Some programs run in a server-like mode where the spawned process does not
directly own the window that is created, which is instead created by a
background process. In this case, the window may not be correctly caught in the
scratchpad group. To work around this, you can pass a ``config.Match`` object
to the corresponding ``Dropdown``. See below.
Reference
---------
.. qtile_class:: libqtile.config.ScratchPad
:no-commands:
.. qtile_class:: libqtile.config.DropDown
:no-commands:

View File

@ -59,10 +59,12 @@ We can then subscribe to ``startup_once`` to run this script:
import os
import subprocess
from libqtile import hook
@hook.subscribe.startup_once
def autostart():
home = os.path.expanduser('~/.config/qtile/autostart.sh')
subprocess.call([home])
subprocess.run([home])
Accessing the qtile object
--------------------------

View File

@ -23,13 +23,12 @@ config, if it doesn't exist yet.
Default Configuration
=====================
The `default configuration
<https://github.com/qtile/qtile/blob/master/libqtile/resources/default_config.py>`_
The :ref:`default configuration<default_config>`
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.
is restarted or the config is reloaded, 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
@ -47,7 +46,7 @@ key. The basic operation is:
layout)
* ``mod + <tab>``: switch layouts
* ``mod + w``: close window
* ``mod + <ctrl> + r``: restart qtile with new config
* ``mod + <ctrl> + r``: reload the config
* ``mod + <group name>``: switch to that group
* ``mod + <shift> + <group name>``: send a window to that group
* ``mod + <enter>``: start terminal guessed by ``libqtile.utils.guess_terminal``
@ -105,24 +104,24 @@ configuration variables that control specific aspects of Qtile's behavior:
* - variable
- default
- description
* - auto_fullscreen
- True
* - ``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.
* - bring_front_click
- False
* - ``bring_front_click``
- ``False``
- When clicked, should the window be brought to the front or not. If this
is set to "floating_only", only floating windows will get affected (This
sets the X Stack Mode to Above.)
* - cursor_warp
- False
* - ``cursor_warp``
- ``False``
- If true, the cursor follows the focus as directed by the keyboard,
warping to the center of the focused window. When switching focus between
screens, If there are no windows in the screen, the cursor will warp to
the center of the screen.
* - dgroups_key_binder
- None
* - ``dgroups_key_binder``
- ``None``
- A function which generates group binding hotkeys. It takes a single
argument, the DGroups object, and can use that to set up dynamic key
bindings.
@ -131,21 +130,21 @@ configuration variables that control specific aspects of Qtile's behavior:
<https://github.com/qtile/qtile/blob/master/libqtile/dgroups.py>`_
called simple_key_binder(), which will bind groups to mod+shift+0-10 by
default.
* - dgroups_app_rules
- []
* - ``dgroups_app_rules``
- ``[]``
- A list of Rule objects which can send windows to various groups based
on matching criteria.
* - extension_defaults
- same as `widget_defaults`
* - ``extension_defaults``
- same as ``widget_defaults``
- Default settings for extensions.
* - floating_layout
- layout.Floating(float_rules=[...])
* - ``floating_layout``
- ``layout.Floating(float_rules=[...])``
- The default floating layout to use. This allows you to set
custom floating rules among other things if you wish.
See the configuration file for the default `float_rules`.
* - focus_on_window_activation
- smart
* - ``focus_on_window_activation``
- ``'smart'``
- Behavior of the _NET_ACTIVATE_WINDOW message sent by applications
- urgent: urgent flag is set for the window
@ -155,21 +154,22 @@ configuration variables that control specific aspects of Qtile's behavior:
- smart: automatically focus if the window is in the current group
- never: never automatically focus any window that requests it
* - follow_mouse_focus
- True
* - ``follow_mouse_focus``
- ``True``
- Controls whether or not focus follows the mouse around as it moves
across windows in a layout.
* - widget_defaults
- dict(font='sans',
fontsize=12,
padding=3)
- Default settings for bar widgets.
* - reconfigure_screens
- True
* - ``widget_defaults``
- ``dict(font='sans', fontsize=12, padding=3)``
- Default settings for bar widgets. Note: if the font file
associated with the font selected here is modified while Qtile
is running, Qtile may segfault (for details see `issue #2656
<https://github.com/qtile/qtile/issues/2656>`_).
* - ``reconfigure_screens``
- ``True``
- Controls whether or not to automatically reconfigure screens when there
are changes in randr output configuration.
* - wmname
- "LG3D"
* - ``wmname``
- ``'LG3D'``
- Gasp! We're lying here. In fact, nobody really uses or cares
about this string besides java UI toolkits; you can see several
discussions on the mailing lists, GitHub issues, and other WM
@ -178,8 +178,8 @@ configuration variables that control specific aspects of Qtile's behavior:
we're a working one by default. We choose LG3D to maximize irony:
it is a 3D non-reparenting WM written in java that happens to be
on java's whitelist.
* - auto_minimize
- True
* - ``auto_minimize``
- ``True``
- If things like steam games want to auto-minimize themselves when losing
focus, should we respect this or not?

View File

@ -46,7 +46,7 @@ The :class:`EzKey` modifier keys (i.e. ``MASC``) can be overwritten through the
}
Callbacks can also be configured to work only under certain conditions by using
the ``when()`` method. Currently, two conditions are supported:
the ``when()`` method. Currently, the following conditions are supported:
::
@ -63,6 +63,10 @@ the ``when()`` method. Currently, two conditions are supported:
# Limit action to when the current window is not floating (default True)
Key([mod], "f", lazy.window.toggle_fullscreen().when(when_floating=False))
# Also matches are supported on the current window
# For example to match on the wm_class for fullscreen do the following
Key([mod], "f", lazy.window.toggle_fullscreen().when(focused=Match(wm_class="yourclasshere"))
]
KeyChords

View File

@ -7,7 +7,7 @@ Lazy objects
The ``lazy.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 :ref:`qshell`.
call the command through a script or through :ref:`qtile-shell`.
Example
-------
@ -50,8 +50,10 @@ General functions
- Run the ``application``
* - ``lazy.spawncmd()``
- Open command prompt on the bar. See prompt widget.
* - ``lazy.reload_config()``
- Reload the config.
* - ``lazy.restart()``
- Restart Qtile and reload its config. It won't close your windows
- Restart Qtile. In X11, it won't close your windows.
* - ``lazy.shutdown()``
- Close the whole Qtile
@ -80,9 +82,9 @@ Group functions
- Switch window focus to previous window in group
* - ``lazy.group["group_name"].toscreen()``
- Move to the group called ``group_name``.
Takes an optional ``toggle`` parameter (defaults to True).
If this group is already on the screen, then the group is toggled
with last used
Takes an optional ``toggle`` parameter (defaults to False).
If this group is already on the screen, it does nothing by default;
to toggle with the last used group instead, use ``toggle=True``.
* - ``lazy.layout.increase_ratio()``
- Increase the space for master window at the expense of slave windows
* - ``lazy.layout.decrease_ratio()``
@ -120,6 +122,10 @@ ScratchPad DropDown functions
* - ``lazy.group["group_name"].dropdown_toggle("name")``
- Toggles the visibility of the specified DropDown window.
On first use, the configured process is spawned.
* - ``lazy.group["group_name"].hide_all()``
- Hides all DropDown windows.
* - ``lazy.group["group_name"].dropdown_reconfigure("name", **configuration)``
- Update the configuration of the named DropDown.
User-defined functions
----------------------
@ -133,3 +139,67 @@ User-defined functions
* - ``lazy.function(func, *args, **kwargs)``
- Calls ``func(qtile, *args, **kwargs)``. NB. the ``qtile`` object is
automatically passed as the first argument.
Examples
--------
``lazy.function`` can also be used as a decorator for functions.
::
from libqtile.config import Key
from libqtile.command import lazy
@lazy.function
def my_function(qtile):
...
keys = [
Key(
["mod1"], "k",
my_function
)
]
Additionally, you can pass arguments to user-defined function in one of two ways:
1) In-line definition
Arguments can be added to the ``lazy.function`` call.
::
from libqtile.config import Key
from libqtile.command import lazy
from libqtile.log_utils import logger
def multiply(qtile, value, multiplier=10):
logger.warning(f"Multiplication results: {value * multiplier}")
keys = [
Key(
["mod1"], "k",
lazy.function(multiply, 10, multiplier=2)
)
]
2) Decorator
Arguments can also be passed to the decorated function.
::
from libqtile.config import Key
from libqtile.command import lazy
from libqtile.log_utils import logger
@lazy.function
def multiply(qtile, value, multiplier=10):
logger.warning(f"Multiplication results: {value * multiplier}")
keys = [
Key(
["mod1"], "k",
multiply(10, multiplier=2)
)
]

View File

@ -3,9 +3,8 @@ Screens
=======
The ``screens`` configuration variable is where the physical screens, their
associated ``bars``, and the ``widgets`` contained within the bars are defined.
See :ref:`ref-widgets` for a listing of available widgets.
associated ``bars``, and the ``widgets`` contained within the bars are defined
(see :ref:`ref-widgets` for a listing of available widgets).
Example
=======
@ -48,6 +47,33 @@ entire window.
In X11 backends, transparency will be disabled in a bar if the ``background``
color is fully opaque.
Users can add borders to the bar by using the ``border_width`` and
``border_color`` parameters. Providing a single value sets the value for all
four sides while sides can be customised individually by setting four values
in a list (top, right, bottom, left) e.g. ``border_width=[2, 0, 2, 0]`` would
draw a border 2 pixels thick on the top and bottom of the bar.
Multiple Screens
================
You will see from the example above that ``screens`` is a list of individual
``Screen`` objects. The order of the screens in this list should match the order
of screens as seen by your display server.
X11
~~~
You can view the current order of your screens by running ``xrandr --listmonitors``.
Examples of how to set the order of your screens can be found on the
`Arch wiki <https://wiki.archlinux.org/title/Multihead>`_.
Wayland
~~~~~~~
The Wayland backend supports the wlr-output-management protocol for configuration of
outputs by tools such as `Kanshi <https://github.com/emersion/kanshi>`_.
Fake Screens
============

View File

@ -25,14 +25,25 @@ has two qualities:
Ensure to include any appropriate log entries from
``~/.local/share/qtile/qtile.log`` and/or ``~/.xsession-errors``!
Sometimes, an ``xtrace`` is requested. If that is the case, refer to
:ref:`capturing an xtrace <capturing-an-xtrace>`.
Writing code
============
To get started writing code for Qtile, check out our guide to :ref:`hacking`.
A more detailed page on creating widgets is available :ref:`here <widget-creation>`.
.. important::
Use a separate **git branch** to make rebasing easy. Ideally, you would
``git checkout -b <my_feature_branch_name>`` before starting your work.
See also: :ref:`using git <using-git>`.
.. _submitting-a-pr:
Submit a pull request
---------------------
@ -48,7 +59,39 @@ to our `issue tracker <https://github.com/qtile/qtile/issues>`_ on GitHub.
* **Code** that conforms to PEP8.
* **Unit tests** that pass locally and in our CI environment (More below).
*Please add unit tests* to ensure that your code works and stays working!
* **Documentation** updates on an as needed basis.
* A ``qtile migrate`` **migration** is required for config-breaking changes.
See `migrate.py <https://github.com/qtile/qtile/blob/libqtile/scripts/migrate.py>`_
for examples and consult the `bowler documentation <https://pybowler.io>`_
for detailed help and documentation.
* **Code** that does not include *unrelated changes*. Examples for this are
formatting changes, replacing quotes or whitespace in other parts of the
code or "fixing" linter warnings popping up in your editor on existing
code. *Do not include anything like the above!*
* **Widgets** don't need to catch their own exceptions, or introduce their
own polling infrastructure. The code in ``libqtile.widget.base.*`` does
all of this. Your widget should generally only include whatever
parsing/rendering code is necessary, any other changes should go at the
framework level. Make sure to double-check that you are not
re-implementing parts of ``libqtile.widget.base``.
* **Commit messages** are more important that Github PR notes, since this is
what people see when they are spelunking via ``git blame``. Please include
all relevant detail in the actual git commit message (things like exact
stack traces, copy/pastes of discussion in IRC/mailing lists, links to
specifications or other API docs are all good). If your PR fixes a Github
issue, it might also be wise to link to it with ``#1234`` in the commit
message.
* PRs with **multiple commits** should not introduce code in one patch to
then change it in a later patch. Please do a patch-by-patch review of your
PR, and make sure each commit passes CI and makes logical sense on its
own. In other words: *do* introduce your feature in one commit and maybe
add the tests and documentation in a seperate commit. *Don't* push commits
that partially implement a feature and are basically broken.
.. note:: Others might ban *force-pushes*, we allow them and prefer them over
incomplete commits or commits that have a bad and meaningless commit
description.
Feel free to add your contribution (no matter how small) to the appropriate
place in the CHANGELOG as well!
@ -75,6 +118,8 @@ For any Qtile-specific question on testing, feel free to ask on our `issue
tracker <https://github.com/qtile/qtile/issues>`_ or on IRC (#qtile on
irc.oftc.net).
.. _running-tests-locally:
Running tests locally
---------------------

View File

@ -75,4 +75,51 @@ 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.
How can I get my groups to stick to screens?
============================================
This behaviour can be replicated by configuring your keybindings to not move
groups between screens. For example if you want groups ``"1"``, ``"2"`` and
``"3"`` on one screen and ``"q"``, ``"w"``, and ``"e"`` on the other, instead
of binding keys to ``lazy.group[name].toscreen()``, use this:
.. code-block:: python
def go_to_group(name: str) -> Callable:
def _inner(qtile: Qtile) -> None:
if len(qtile.screens) == 1:
qtile.groups_map[name].cmd_toscreen()
return
if name in '123':
qtile.focus_screen(0)
qtile.groups_map[name].cmd_toscreen()
else:
qtile.focus_screen(1)
qtile.groups_map[name].cmd_toscreen()
return _inner
for i in groups:
keys.append(Key([mod], i.name, lazy.function(go_to_group(i.name))))
If you use the ``GroupBox`` widget you can make it reflect this behaviour:
.. code-block:: python
groupbox1 = widget.GroupBox(visible_groups=['1', '2', '3'])
groupbox2 = widget.GroupBox(visible_groups=['q', 'w', 'e'])
And if you jump between having single and double screens then modifying the
visible groups on the fly may be useful:
.. code-block:: python
@hook.subscribe.screens_reconfigured
async def _():
if len(qtile.screens) > 1:
groupbox1.visible_groups = ['1', '2', '3']
else:
groupbox1.visible_groups = ['1', '2', '3', 'q', 'w', 'e']
if hasattr(groupbox1, 'bar'):
groupbox1.bar.draw()

View File

@ -20,6 +20,7 @@ imagemagick>=6.8 imagemagick ``test/test_images*`` (optional)
gtk-layer-shell libgtk-layer-shell0 Testing notification windows in Wayland (optional)
dbus-launch dbus-x11 Testing dbus-using widgets (optional)
notifiy-send libnotify-bin Testing ``Notify`` widget (optional)
xvfb xvfb Testing with X11 headless (optional)
================= =================== ==================================================
.. _pytest: https://docs.pytest.org
@ -38,7 +39,7 @@ both backends, specify as arguments to pytest:
pytest --backend wayland # Test just Wayland backend
pytest --backend x11 --backend wayland # Test both
Testing with the X11 backend requires Xephyr_ in addition to the core
Testing with the X11 backend requires Xephyr_ (and xvfb for headless mode) in addition to the core
dependencies.
@ -102,6 +103,30 @@ to ensure your patch complies with reasonable formatting constraints. We also
request that git commit messages follow the
`standard format <https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html>`_.
Logging
=======
Logs are important to us because they are our best way to see what Qtile is
doing when something abnormal happens. However, our goal is not to have as many
logs as possible, as this hinders readability. What we want are relevant logs.
To decide which log level to use, refer to the following scenarios:
* ERROR: a problem affects the behavior of Qtile in a way that is noticeable to
the end user, and we can't work around it.
* WARNING: a problem causes Qtile to operate in a suboptimal manner.
* INFO: the state of Qtile has changed.
* DEBUG: information is worth giving to help the developer better understand
which branch the process is in.
Be careful not to overuse DEBUG and clutter the logs. No information should be
duplicated between two messages.
Also, keep in mind that any other level than DEBUG is aimed at users who don't
necessarily have advanced programming knowledge; adapt your message
accordingly. If it can't make sense to your grandma, it's probably meant to be
a DEBUG message.
Deprecation policy
==================

122
docs/manual/howto/git.rst Normal file
View File

@ -0,0 +1,122 @@
.. _using-git:
=============
Using ``git``
=============
``git`` is the version control system that is used to manage all of the source
code. It is very powerful, but might be frightening at first.
This page should give you a quick overview, but for a complete guide you will
have to search the web on your own.
Another great resource to get started practically without having to try out the
newly-learned commands on a pre-existing repository is
`learn git branching <https://learngitbranching.js.org>`_.
You should probably learn the basic ``git`` vocabulary and then come back to
find out how you can use all that practically. This guide will be oriented on
how to create a pull request and things might be in a different order compared
to the introductory guides.
.. warning:: This guide is not complete and never will be. If something isn't
clear, consult other sources until you are confident you know what you are
doing.
I want to try out a feature somebody is working on
==================================================
If you see a pull request on `GitHub <https://www.github.com/qtile/qtile/pulls>`_
that you want to try out, have a look at the line where it says::
user wants to merge n commits into qtile:master from user:branch
Right now you probably have one *remote* from which you can fetch changes, the
``origin``. If you cloned ``qtile/qtile``, ``git remote show origin`` will spit
out the *upstream* url. If you cloned your fork, ``origin`` points to it and you
probably want to ``git remote add upstream https://www.github.com/qtile/qtile``.
To try out somebody's work, you can add their fork as a new remote::
git remote add <user> https://www.github.com/user/qtile
where you fill in the username from the line we asked you to search for before.
Then you can load data from that remote with ``git fetch`` and then ultimately
check out the branch with ``git checkout <user>/<branch>``.
**Alternatively**, it is also possible to fetch and checkout pull requests
without needing to add other remotes. The upstream remote is sufficient::
git fetch upstream pull/<id>/head:pr<id>
git checkout pr<id>
The numeric pull request id can be found in the url or next to the title
(preceeded by a # symbol).
.. note:: Having the feature branch checked out doesn't mean that it is
installed and will be loaded when you restart qtile. You might still need to
install it with ``pip``.
I committed changes and the tests failed
========================================
You can easily change your last commit: After you have done your work,
``git add`` everything you need and use ``git commit --amend`` to change your
last commit. This causes the git history of your local clone to be diverged from
your fork on GitHub, so you need to force-push your changes with::
git push -f <origin> <feature-branch>
where origin might be your user name or ``origin`` if you cloned your fork and
feature-branch is to be replaced by the name of the branch you are working on.
Assuming the feature branch is currently checked out, you can usually omit it
and just specify the origin.
I was told to rebase my work
============================
If *upstream/master* is changed and you happened to change the same files as the
commits that were added upstream, you should rebase your work onto the most
recent *upstream/master*. Checkout your master, pull from *upstream*, checkout
your branch again and then rebase it::
git checkout master
git pull upstream/master
git checkout <feature-branch>
git rebase upstream/master
You will be asked to solve conflicts where your diff cannot be applied with
confidence to the work that was pushed upstream. If that is the case, open the
files in your text editor and resolve the conflicts manually. You possibly need
to ``git rebase --continue`` after you have resolved conflicts for one commit if
you are rebasing multiple commits.
Note that the above doesn't work if you didn't create a branch. In that case you
will find guides elsewhere to fix this problem, ideally by creating a branch and
resetting your master branch to where it should be.
I was told to squash some commits
=================================
If you introduce changes in one commit and replace them in another, you are told
to squash these changes into one single commit without the intermediate step::
git rebase -i master
opens a text editor with your commits and a comment block reminding you what you
can do with your commits. You can reword them to change the commit message,
reorder them or choose ``fixup`` to squash the changes of a commit into the
commit on the line above.
This also changes your git history and you will need to force-push your changes
afterwards.
Note that interactive rebasing also allows you to split, reorder and edit
commits.
I was told to edit a commit message
===================================
If you need to edit the commit message of the last commit you did, use::
git commit --amend
to open an editor giving you the possibility to reword the message. If you want
to reword the message of an older commit or multiple commits, use
``git rebase -i`` as above with the ``reword`` command in the editor.

View File

@ -201,13 +201,14 @@ components that make up the widget. Examples of displaying text, icons and
drawings are set out below.
It is important to note that the bar controls the placing of the widget by
assigning the ``offsetx`` value (for horizontal orientations) and ``offsety``
value (for vertical orientations). Widgets should use this at the end of the
``draw`` method.
assigning the ``offsetx`` value (for horizontal positioning) and ``offsety``
value (for vertical positioning). Widgets should use this at the end of the
``draw`` method. Both ``offsetx`` and ``offsety`` are required as both values will
be set if the bar is drawing a border.
.. code:: python
self.drawer.draw(offsetx=self.offsetx, width=self.width)
self.drawer.draw(offsetx=self.offsetx, offsety=self.offsety, width=self.width)
.. note::

View File

@ -39,4 +39,4 @@ 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
.. _Funtoo Qtile documentation: https://www.funtoo.org/Package:Qtile

View File

@ -128,14 +128,5 @@ window:
qtile start -b wayland
If you want your config file to work with different backends but want some
options set differently per backend, something like this may be useful:
.. code-block:: python
from libqtile import qtile
if qtile.core.name == "x11":
term = "urxvt"
elif qtile.core.name == "wayland":
term = "foot"
See the :ref:`Wayland <wayland>` page for more information on running Qtile as
a Wayland compositor.

View File

@ -5,10 +5,10 @@ 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 <ref-layouts>` and
:ref:`widgets <ref-widgets>`, may implement their own set of commands beyond
those given here.
tree when running ``qtile shell`` or scripting commands to qtile. Note that
this is an incomplete list, some objects, such as :ref:`layouts <ref-layouts>`
and :ref:`widgets <ref-widgets>`, may implement their own set of commands
beyond those given here.
.. _qtile_commands:
@ -23,4 +23,4 @@ those given here.
.. qtile_class:: libqtile.config.Screen
:noindex:
.. qtile_class:: libqtile.backend.x11.window.Window
.. qtile_class:: libqtile.backend.base.Window

View File

@ -1,5 +1,3 @@
.. _ref-extensions:
===================
Built-in Extensions
===================

View File

@ -1,11 +0,0 @@
=========
Reference
=========
.. toctree::
:maxdepth: 1
hooks
layouts
widgets
extensions

View File

@ -33,6 +33,8 @@ traceback of an error to the log. By sticking these amongst your changes you
can look more closely at the effects of any changes you made to Qtile's
internals.
.. _capturing-an-xtrace:
Capturing an ``xtrace``
=======================

64
docs/manual/wayland.rst Normal file
View File

@ -0,0 +1,64 @@
=====================================
Running Qtile as a Wayland Compositor
=====================================
.. _wayland:
Some functionality may not yet be implemented in the Wayland compositor. Please
see the discussion `here <https://github.com/qtile/qtile/discussions/2409>`_ to
see the current state of development.
Backend-Specific Configuration
==============================
If you want your config file to work with different backends but want some
options set differently per backend, you can check the name of the current
backend in your config as follows:
.. code-block:: python
from libqtile import qtile
if qtile.core.name == "x11":
term = "urxvt"
elif qtile.core.name == "wayland":
term = "foot"
Keyboard Configuration
======================
Keyboard management is done using `xkbcommon
<https://github.com/xkbcommon/libxkbcommon>`_ via the `Python bindings
<https://github.com/sde1000/python-xkbcommon>`_. xkbcommon's initial
configuration can be set using environmental variables; see `their docs
<https://xkbcommon.org/doc/current/group__context.html>`_ for more information.
The 5 ``XKB_DEFAULT_X`` environmental variables have corresponding settings in
X11's keyboard configuration, so if you have these defined already simply copy
their values into these variables, otherwise see `X11's helpful XKB guide
<https://www.x.org/releases/X11R7.5/doc/input/XKB-Config.html>`_ to see the
syntax for these settings. Simply set these variables before starting Qtile and
the initial keyboard state will match these settings.
If you want to change keyboard configuration during runtime, you can use the
core's `set_keymap` command (see :ref:`wayland-cmds` below).
Running X11-Only Programs
=========================
Qtile does not support XWayland directly and there are no plans to implement
XWayland support. Instead, the recommended way to run any programs that do not
support Wayland themselves is to run them inside the `cage
<https://github.com/Hjdskes/cage>`_ Wayland compositor, which will contain the
program inside a window that does support XWayland. Otherwise, you could find
alternatives that support Wayland directly.
.. _wayland-cmds:
Core Commands
=============
.. qtile_class:: libqtile.backend.wayland.core.Core

View File

@ -1,6 +1,7 @@
setuptools_scm
sphinx
sphinx_rtd_theme
funcparserlib==1.0.0a0
sphinxcontrib-seqdiag
numpydoc

View File

@ -63,7 +63,7 @@ qtile_class_template = Template('''
{% for key, default, description in defaults %}
* - ``{{ key }}``
- ``{{ default }}``
- {{ description }}
- {{ description[1:-1] }}
{% endfor %}
{% endif %}
{% if commandable %}

View File

@ -9,7 +9,8 @@ from abc import ABCMeta, abstractmethod
import cairocffi
from libqtile import drawer, pangocffi, utils
from libqtile.command.base import CommandObject
from libqtile.command.base import CommandError, CommandObject
from libqtile.log_utils import logger
if typing.TYPE_CHECKING:
from typing import Any, Dict, List, Optional, Tuple, Union
@ -18,11 +19,12 @@ if typing.TYPE_CHECKING:
from libqtile.command.base import ItemT
from libqtile.core.manager import Qtile
from libqtile.group import _Group
from libqtile.utils import ColorType
from libqtile.utils import ColorsType
class Core(metaclass=ABCMeta):
class Core(CommandObject, metaclass=ABCMeta):
painter: Any
supports_restarting: bool = True
@property
@abstractmethod
@ -30,6 +32,12 @@ class Core(metaclass=ABCMeta):
"""The name of the backend"""
pass
def _items(self, name: str) -> ItemT:
return None
def _select(self, name, sel):
return None
@abstractmethod
def finalize(self):
"""Destructor/Clean up resources"""
@ -82,8 +90,8 @@ class Core(metaclass=ABCMeta):
def ungrab_pointer(self) -> None:
"""Release grabbed pointer events"""
def scan(self) -> None:
"""Scan for clients if required."""
def distribute_windows(self, initial: bool) -> None:
"""Distribute windows to groups. `initial` will be `True` if Qtile just started."""
def warp_pointer(self, x: int, y: int) -> None:
"""Warp the pointer to the given coordinates relative."""
@ -113,9 +121,12 @@ class Core(metaclass=ABCMeta):
"""Get the keysym for a key from its name"""
raise NotImplementedError
def change_vt(self, vt: int) -> bool:
"""Change virtual terminal, returning success."""
return False
def cmd_info(self) -> Dict:
"""Get basic information about the running backend."""
return {
"backend": self.name,
"display_name": self.display_name
}
@enum.unique
@ -206,7 +217,14 @@ class _Window(CommandObject, metaclass=ABCMeta):
class Window(_Window, metaclass=ABCMeta):
"""A regular Window belonging to a client."""
"""
A regular Window belonging to a client.
Abstract methods are required to be defined as part of a specific backend's
implementation. Non-abstract methods have default implementations here to be shared
across backends.
"""
qtile: Qtile
# If float_x or float_y are None, the window has never floated
float_x: Optional[int]
@ -240,6 +258,16 @@ class Window(_Window, metaclass=ABCMeta):
"""Does this window want to be fullscreen?"""
return False
@property
def opacity(self) -> float:
"""The opacity of this window from 0 (transparent) to 1 (opaque)."""
return self._opacity
@opacity.setter
def opacity(self, opacity: float) -> None:
"""Opacity setter."""
self._opacity = opacity
def match(self, match: config.Match) -> bool:
"""Compare this window against a Match instance."""
return match.compare(self)
@ -273,13 +301,16 @@ class Window(_Window, metaclass=ABCMeta):
def get_pid(self) -> int:
"""Return the PID that owns the window."""
def paint_borders(self, color: Union[ColorType, List[ColorType]], width: int) -> None:
def paint_borders(self, color: ColorsType, width: int) -> None:
"""Paint the window borders with the given color(s) and width"""
@abstractmethod
def cmd_focus(self, warp: bool = True) -> None:
"""Focuses the window."""
def cmd_match(self, *args, **kwargs) -> bool:
return self.match(*args, **kwargs)
@abstractmethod
def cmd_get_position(self) -> Tuple[int, int]:
"""Get the (x, y) of the window"""
@ -300,6 +331,13 @@ class Window(_Window, metaclass=ABCMeta):
def cmd_set_position_floating(self, x: int, y: int) -> None:
"""Move window to x and y"""
@abstractmethod
def cmd_set_position(self, x: int, y: int) -> None:
"""
Move floating window to x and y; swap tiling window with the window under the
pointer.
"""
@abstractmethod
def cmd_set_size_floating(self, w: int, h: int) -> None:
"""Set window dimensions to w and h"""
@ -323,7 +361,11 @@ class Window(_Window, metaclass=ABCMeta):
@abstractmethod
def cmd_toggle_maximize(self) -> None:
"""Toggle the fullscreen state of the window."""
"""Toggle the maximize state of the window."""
@abstractmethod
def cmd_toggle_minimize(self) -> None:
"""Toggle the minimize state of the window."""
@abstractmethod
def cmd_toggle_fullscreen(self) -> None:
@ -342,16 +384,55 @@ class Window(_Window, metaclass=ABCMeta):
"""Bring the window to the front"""
def cmd_togroup(
self, group_name: Optional[str] = None, *, switch_group: bool = False
self,
group_name: Optional[str] = None,
groupName: Optional[str] = None, # Deprecated # noqa: N803
switch_group: bool = False
) -> None:
"""Move window to a specified group
Also switch to that group if switch_group is True.
Also switch to that group if `switch_group` is True.
`groupName` is deprecated and will be dropped soon. Please use `group_name`
instead.
"""
if groupName is not None:
logger.warning(
"Window.cmd_togroup's groupName is deprecated; use group_name"
)
group_name = groupName
self.togroup(group_name, switch_group=switch_group)
def cmd_opacity(self, opacity):
"""Set the window's opacity"""
def cmd_toscreen(self, index: Optional[int] = None) -> 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)
"""
if index is None:
screen = self.qtile.current_screen
else:
try:
screen = self.qtile.screens[index]
except IndexError:
raise CommandError('No such screen: %d' % index)
self.togroup(screen.group.name)
def cmd_opacity(self, opacity: float) -> None:
"""Set the window's opacity.
The value must be between 0 and 1 inclusive.
"""
if opacity < .1:
self.opacity = .1
elif opacity > 1:
@ -359,16 +440,16 @@ class Window(_Window, metaclass=ABCMeta):
else:
self.opacity = opacity
def cmd_down_opacity(self):
"""Decrease the window's opacity"""
def cmd_down_opacity(self) -> None:
"""Decrease the window's opacity by 10%."""
if self.opacity > .2:
# don't go completely clear
self.opacity -= .1
else:
self.opacity = .1
def cmd_up_opacity(self):
"""Increase the window's opacity"""
def cmd_up_opacity(self) -> None:
"""Increase the window's opacity by 10%."""
if self.opacity < .9:
self.opacity += .1
else:
@ -475,6 +556,7 @@ class Drawer:
self.current_rect = (0, 0, 0, 0)
self.previous_rect = (-1, -1, -1, -1)
self._enabled = True
def finalize(self):
"""Destructor/Clean up resources"""
@ -518,6 +600,14 @@ class Drawer:
)
self.ctx = self.new_ctx()
def _check_surface_reset(self):
"""
Checks to see if the widget is not being reflected and
then clears RecordingSurface of operations.
"""
if not self.mirrors:
self._reset_surface()
@property
def needs_update(self) -> bool:
# We can't test for the surface's ink_extents here on its own as a completely
@ -579,12 +669,53 @@ class Drawer:
self.ctx.fill()
self.ctx.stroke()
def enable(self):
"""Enable drawing of surface to Internal window."""
self._enabled = True
def disable(self):
"""Disable drawing of surface to Internal window."""
self._enabled = False
def draw(
self,
offsetx: int = 0,
offsety: int = 0,
width: Optional[int] = None,
height: Optional[int] = None,
):
"""
A wrapper for the draw operation.
This draws our cached operations to the Internal window.
If Drawer has been disabled then the RecordingSurface will
be cleared if no mirrors are waiting to copy its contents.
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.
"""
if self._enabled:
self._draw(offsetx, offsety, width, height)
# Check to see if RecordingSurface can be cleared.
self._check_surface_reset()
def _draw(
self,
offsetx: int = 0,
offsety: int = 0,
width: Optional[int] = None,
height: Optional[int] = None,
):
"""
This draws our cached operations to the Internal window.
@ -605,7 +736,7 @@ class Drawer:
def new_ctx(self):
return pangocffi.patch_cairo_context(cairocffi.Context(self.surface))
def set_source_rgb(self, colour: Union[ColorType, List[ColorType]], ctx: cairocffi.Context = None):
def set_source_rgb(self, colour: ColorsType, ctx: cairocffi.Context = None):
# If an alternate context is not provided then we draw to the
# drawer's default context
if ctx is None:
@ -613,7 +744,7 @@ class Drawer:
if isinstance(colour, list):
if len(colour) == 0:
# defaults to black
ctx.set_source_rgba(*utils.rgb("#000000"))
ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
elif len(colour) == 1:
ctx.set_source_rgba(*utils.rgb(colour[0]))
else:
@ -621,10 +752,7 @@ class Drawer:
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)
linear.add_color_stop_rgba(step, *utils.rgb(c))
step += step_size
ctx.set_source(linear)
else:

View File

@ -33,9 +33,11 @@ from wlroots.wlr_types import (
Cursor,
DataControlManagerV1,
DataDeviceManager,
ForeignToplevelManagerV1,
GammaControlManagerV1,
OutputLayout,
PrimarySelectionV1DeviceManager,
RelativePointerManagerV1,
ScreencopyManagerV1,
Surface,
XCursorManager,
@ -56,6 +58,10 @@ from wlroots.wlr_types.output_management_v1 import (
OutputConfigurationV1,
OutputManagerV1,
)
from wlroots.wlr_types.pointer_constraints_v1 import (
PointerConstraintsV1,
PointerConstraintV1,
)
from wlroots.wlr_types.server_decoration import (
ServerDecorationManager,
ServerDecorationManagerMode,
@ -74,15 +80,18 @@ from libqtile.backend.wayland.output import Output
from libqtile.log_utils import logger
if typing.TYPE_CHECKING:
from typing import List, Optional, Tuple, Union
from typing import List, Optional, Sequence, Set, Tuple, Union
from wlroots.wlr_types import Output as wlrOutput
from wlroots.wlr_types.data_device_manager import Drag
from libqtile import config
from libqtile.core.manager import Qtile
class Core(base.Core, wlrq.HasListeners):
supports_restarting: bool = False
def __init__(self):
"""Setup the Wayland core backend"""
self.qtile: Optional[Qtile] = None
@ -104,7 +113,7 @@ class Core(base.Core, wlrq.HasListeners):
# mapped_windows contains just regular windows
self.mapped_windows: List[window.WindowType] = [] # Ascending in Z
# stacked_windows also contains layer_shell windows from the current output
self.stacked_windows: List[window.WindowType] = [] # Ascending in Z
self.stacked_windows: Sequence[window.WindowType] = [] # Ascending in Z
self._current_output: Optional[Output] = None
# set up inputs
@ -112,11 +121,14 @@ class Core(base.Core, wlrq.HasListeners):
self.grabbed_keys: List[Tuple[int, int]] = []
self.grabbed_buttons: List[Tuple[int, int]] = []
DataDeviceManager(self.display)
self.live_dnd: Optional[wlrq.Dnd] = None
DataControlManagerV1(self.display)
self.seat = seat.Seat(self.display, "seat0")
self.add_listener(
self.seat.request_set_selection_event, self._on_request_set_selection
)
self.add_listener(self.seat.request_start_drag_event, self._on_request_start_drag)
self.add_listener(self.seat.start_drag_event, self._on_start_drag)
self.add_listener(self.backend.new_input_event, self._on_new_input)
# set up outputs
@ -172,6 +184,15 @@ class Core(base.Core, wlrq.HasListeners):
# wlr_server_decoration will be removed in a future version of wlroots
server_decoration_manager = ServerDecorationManager.create(self.display)
server_decoration_manager.set_default_mode(ServerDecorationManagerMode.SERVER)
pointer_constraints_v1 = PointerConstraintsV1(self.display)
self.add_listener(
pointer_constraints_v1.new_constraint_event,
self._on_new_pointer_constraint,
)
self.pointer_constraints: Set[wlrq.PointerConstraint] = set()
self.active_pointer_constraint: Optional[wlrq.PointerConstraint] = None
self._relative_pointer_manager_v1 = RelativePointerManagerV1(self.display)
self.foreign_toplevel_manager_v1 = ForeignToplevelManagerV1.create(self.display)
# start
os.environ["WAYLAND_DISPLAY"] = self.socket.decode()
@ -207,6 +228,18 @@ class Core(base.Core, wlrq.HasListeners):
self.seat.set_selection(event._ptr.source, event.serial)
logger.debug("Signal: seat request_set_selection")
def _on_request_start_drag(self, _listener, event: seat.RequestStartDragEvent):
logger.debug("Signal: seat request_start_drag")
if not self.live_dnd and self.seat.validate_pointer_grab_serial(event.origin, event.serial):
self.seat.start_pointer_drag(event.drag, event.serial)
else:
event.drag.source.destroy()
def _on_start_drag(self, _listener, event: Drag):
logger.debug("Signal: seat start_drag")
self.live_dnd = wlrq.Dnd(self, event)
def _on_new_input(self, _listener, device: input_device.InputDevice):
logger.debug("Signal: backend new_input_event")
if device.device_type == input_device.InputDeviceType.POINTER:
@ -277,41 +310,65 @@ class Core(base.Core, wlrq.HasListeners):
self.pending_windows.append(win)
def _on_cursor_axis(self, _listener, event: pointer.PointerEventAxis):
self.seat.pointer_notify_axis(
event.time_msec,
event.orientation,
event.delta,
event.delta_discrete,
event.source,
)
handled = False
if event.delta != 0:
if event.orientation == pointer.AxisOrientation.VERTICAL:
button = 5 if 0 < event.delta else 4
else:
button = 7 if 0 < event.delta else 6
self._process_cursor_button(button, True)
handled = self._process_cursor_button(button, True)
if not handled:
self.seat.pointer_notify_axis(
event.time_msec,
event.orientation,
event.delta,
event.delta_discrete,
event.source,
)
def _on_cursor_frame(self, _listener, _data):
self.seat.pointer_notify_frame()
def _on_cursor_button(self, _listener, event: pointer.PointerEventButton):
assert self.qtile is not None
self.seat.pointer_notify_button(
event.time_msec, event.button, event.button_state
)
pressed = event.button_state == input_device.ButtonState.PRESSED
if pressed:
self._focus_by_click()
handled = False
if event.button in wlrq.buttons:
button = wlrq.buttons.index(event.button) + 1
self._process_cursor_button(button, pressed)
handled = self._process_cursor_button(button, pressed)
if not handled:
self.seat.pointer_notify_button(
event.time_msec, event.button, event.button_state
)
def _on_cursor_motion(self, _listener, event: pointer.PointerEventMotion):
assert self.qtile is not None
self.cursor.move(event.delta_x, event.delta_y, input_device=event.device)
self._process_cursor_motion(event.time_msec)
dx = event.delta_x
dy = event.delta_y
# Send relative pointer events to seat - used e.g. by games that have
# constrained cursor movement but want movement events
self._relative_pointer_manager_v1.send_relative_motion(
self.seat, event.time_msec * 1000, dx, dy,
event.unaccel_delta_x, event.unaccel_delta_y
)
if self.active_pointer_constraint:
if not self.active_pointer_constraint.rect.contains_point(
self.cursor.x + dx,
self.cursor.y + dy
):
return
self.cursor.move(dx, dy, input_device=event.device)
self._process_cursor_motion(event.time_msec, self.cursor.x, self.cursor.y)
def _on_cursor_motion_absolute(
self, _listener, event: pointer.PointerEventMotionAbsolute
@ -323,7 +380,17 @@ class Core(base.Core, wlrq.HasListeners):
event.y,
input_device=event.device,
)
self._process_cursor_motion(event.time_msec)
self._process_cursor_motion(event.time_msec, self.cursor.x, self.cursor.y)
def _on_new_pointer_constraint(self, _listener, wlr_constraint: PointerConstraintV1):
logger.debug("Signal: pointer_constraints new_constraint")
constraint = wlrq.PointerConstraint(self, wlr_constraint)
self.pointer_constraints.add(constraint)
if self.seat.pointer_state.focused_surface == wlr_constraint.surface:
if self.active_pointer_constraint:
self.active_pointer_constraint.disable()
constraint.enable()
def _on_new_virtual_keyboard(self, _listener, virtual_keyboard: VirtualKeyboardV1):
self._add_new_keyboard(virtual_keyboard.input_device)
@ -389,18 +456,23 @@ class Core(base.Core, wlrq.HasListeners):
config.send_failed()
config.destroy()
hook.fire("screen_change", None)
hook.fire("screens_reconfigured")
def _process_cursor_motion(self, time):
self.qtile.process_button_motion(self.cursor.x, self.cursor.y)
def _process_cursor_motion(self, time_msec: int, cx: float, cy: float):
assert self.qtile
cx_int = int(cx)
cy_int = int(cy)
self.qtile.process_button_motion(cx_int, cy_int)
if len(self.outputs) > 1:
current_output = self.output_layout.output_at(
self.cursor.x, self.cursor.y
).data
current_output = self.output_layout.output_at(cx, cy).data
if self._current_output is not current_output:
self._current_output = current_output
self.stack_windows()
if self.live_dnd:
self.live_dnd.position(cx, cy)
found = self._under_pointer()
if found:
@ -408,65 +480,62 @@ class Core(base.Core, wlrq.HasListeners):
if isinstance(win, window.Internal):
if self._hovered_internal is win:
win.process_pointer_motion(
self.cursor.x - self._hovered_internal.x,
self.cursor.y - self._hovered_internal.y,
cx_int - self._hovered_internal.x,
cy_int - self._hovered_internal.y,
)
else:
if self._hovered_internal:
self._hovered_internal.process_pointer_leave(
self.cursor.x - self._hovered_internal.x,
self.cursor.y - self._hovered_internal.y,
cx_int - self._hovered_internal.x,
cy_int - self._hovered_internal.y,
)
self.cursor_manager.set_cursor_image("left_ptr", self.cursor)
self.seat.pointer_clear_focus()
win.process_pointer_enter(self.cursor.x, self.cursor.y)
self.seat.pointer_notify_clear_focus()
win.process_pointer_enter(cx_int, cy_int)
self._hovered_internal = win
return
focus_changed = self.seat.pointer_state.focused_surface != surface
if surface is not None:
if surface:
self.seat.pointer_notify_enter(surface, sx, sy)
if focus_changed:
if surface is None:
self.seat.pointer_clear_focus()
if win is not self.qtile.current_window:
hook.fire("client_mouse_enter", win)
if self.qtile.config.follow_mouse_focus:
if isinstance(win, window.Static):
self.qtile.focus_screen(win.screen.index, False)
else:
if win.group.current_window != win:
win.group.focus(win, False)
if (
win.group.screen
and self.qtile.current_screen != win.group.screen
):
self.qtile.focus_screen(win.group.screen.index, False)
self.focus_window(win, surface)
self.seat.pointer_notify_motion(time_msec, sx, sy)
else:
# The enter event contains coordinates, so we only need to
# notify on motion if the focus did not change
self.seat.pointer_notify_motion(time, sx, sy)
self.seat.pointer_notify_clear_focus()
if win is not self.qtile.current_window:
hook.fire("client_mouse_enter", win)
if self.qtile.config.follow_mouse_focus:
if isinstance(win, window.Static):
self.qtile.focus_screen(win.screen.index, False)
else:
if win.group.current_window != win:
win.group.focus(win, False)
if (
win.group.screen
and self.qtile.current_screen != win.group.screen
):
self.qtile.focus_screen(win.group.screen.index, False)
if self._hovered_internal:
self._hovered_internal = None
else:
self.cursor_manager.set_cursor_image("left_ptr", self.cursor)
self.seat.pointer_clear_focus()
self.seat.pointer_notify_clear_focus()
if self._hovered_internal:
self._hovered_internal.process_pointer_leave(
self.cursor.x - self._hovered_internal.x,
self.cursor.y - self._hovered_internal.y,
cx_int - self._hovered_internal.x,
cy_int - self._hovered_internal.y,
)
self._hovered_internal = None
def _process_cursor_button(self, button: int, pressed: bool):
def _process_cursor_button(self, button: int, pressed: bool) -> bool:
assert self.qtile is not None
if pressed:
self.qtile.process_button_click(
button, self.seat.keyboard.modifier, self.cursor.x, self.cursor.y
handled = self.qtile.process_button_click(
button, self.seat.keyboard.modifier,
int(self.cursor.x), int(self.cursor.y)
)
if self._hovered_internal:
@ -476,7 +545,7 @@ class Core(base.Core, wlrq.HasListeners):
button,
)
else:
self.qtile.process_button_release(button, self.seat.keyboard.modifier)
handled = self.qtile.process_button_release(button, self.seat.keyboard.modifier)
if self._hovered_internal:
self._hovered_internal.process_button_release(
@ -485,6 +554,8 @@ class Core(base.Core, wlrq.HasListeners):
button,
)
return handled
def _add_new_pointer(self, device: input_device.InputDevice):
logger.info("Adding new pointer")
self.cursor.attach_input_device(device)
@ -515,6 +586,43 @@ class Core(base.Core, wlrq.HasListeners):
self.event_loop.dispatch(0)
self.display.flush_clients()
def distribute_windows(self, initial: bool) -> None:
if initial:
# This backend does not support restarting
return
assert self.qtile is not None
for win in self.qtile.windows_map.values():
if isinstance(win, (window.Internal, window.Static)):
continue
group = None
assert isinstance(win, window.Window)
if win.group:
if win.group.name in self.qtile.groups_map:
# Put window on group with same name as its old group if one exists
group = self.qtile.groups_map[win.group.name]
else:
# Otherwise place it on the group at the same index
for i, old_group in self.qtile._state.groups: # type: ignore
if i < len(self.qtile.groups):
name = old_group[0]
if win.group.name == name:
group = self.qtile.groups[i]
if group is None:
# Falling back to current group if none found
group = self.qtile.current_group
if win.group and win in win.group.windows:
# It might not be in win.group.windows depending on how group state
# changed across a config reload
win.group.remove(win)
group.add(win)
if group == self.qtile.current_group:
win.unhide()
else:
win.hide()
def new_wid(self) -> int:
"""Get a new unique window ID"""
assert self.qtile is not None
@ -536,6 +644,10 @@ class Core(base.Core, wlrq.HasListeners):
if self.focused_internal:
self.focused_internal = None
if isinstance(win.surface, LayerSurfaceV1):
if not win.surface.current.keyboard_interactive:
return
previous_surface = self.seat.keyboard_state.focused_surface
if previous_surface == surface:
return
@ -545,18 +657,17 @@ class Core(base.Core, wlrq.HasListeners):
previous_xdg_surface = XdgSurface.from_surface(previous_surface)
if not win or win.surface != previous_xdg_surface:
previous_xdg_surface.set_activated(False)
if previous_xdg_surface.data:
previous_xdg_surface.data.set_activated(False)
if not win:
self.seat.keyboard_clear_focus()
return
if isinstance(win.surface, LayerSurfaceV1):
if not win.surface.current.keyboard_interactive:
return
logger.debug("Focussing new window")
if surface.is_xdg_surface and isinstance(win.surface, XdgSurface):
win.surface.set_activated(True)
win.ftm_handle.set_activated(True)
if enter and self.seat.keyboard._ptr: # This pointer is NULL when headless
self.seat.keyboard_notify_enter(surface, self.seat.keyboard)
@ -568,21 +679,22 @@ class Core(base.Core, wlrq.HasListeners):
if found:
win, surface, _, _ = found
if self.qtile.config.bring_front_click:
if (
self.qtile.config.bring_front_click != "floating_only"
or win.floating
):
if self.qtile.config.bring_front_click is True:
win.cmd_bring_to_front()
elif self.qtile.config.bring_front_click == "floating_only":
if not isinstance(win, base.Internal) and win.floating:
win.cmd_bring_to_front()
if not isinstance(win, base.Internal):
if not isinstance(win, base.Static):
if isinstance(win, window.Static):
if win.screen is not self.qtile.current_screen:
self.qtile.focus_screen(win.screen.index, warp=False)
win.focus(False)
else:
if win.group and win.group.screen is not self.qtile.current_screen:
self.qtile.focus_screen(win.group.screen.index, warp=False)
self.qtile.current_group.focus(win, False)
self.focus_window(win, surface=surface, enter=False)
else:
screen = self.qtile.find_screen(self.cursor.x, self.cursor.y)
if screen:
@ -623,11 +735,11 @@ class Core(base.Core, wlrq.HasListeners):
if self._current_output:
layers = self._current_output.layers
self.stacked_windows = (
layers[LayerShellV1Layer.BACKGROUND] + layers[LayerShellV1Layer.BOTTOM]
) # type: ignore
self.stacked_windows += self.mapped_windows
self.stacked_windows += (
layers[LayerShellV1Layer.TOP] + layers[LayerShellV1Layer.OVERLAY]
layers[LayerShellV1Layer.BACKGROUND] +
layers[LayerShellV1Layer.BOTTOM] +
self.mapped_windows + # type: ignore
layers[LayerShellV1Layer.TOP] +
layers[LayerShellV1Layer.OVERLAY]
)
else:
self.stacked_windows = self.mapped_windows
@ -704,13 +816,6 @@ class Core(base.Core, wlrq.HasListeners):
if not self.qtile.windows_map:
break
def change_vt(self, vt: int) -> bool:
"""Change virtual terminal to that specified"""
success = self.backend.get_session().change_vt(vt)
if not success:
logger.warning(f"Could not change VT to: {vt}")
return success
@property
def painter(self):
return wlrq.Painter(self)
@ -724,17 +829,6 @@ class Core(base.Core, wlrq.HasListeners):
assert len(matched) == 1
return matched[0]
def set_keymap(
self, layout: Optional[str], options: Optional[str], variant: Optional[str]
) -> None:
"""
Set the keymap for the current keyboard.
"""
if self.keyboards:
self.keyboards[-1].set_keymap(layout, options, variant)
else:
logger.warning("Could not set keymap: no keyboards set up.")
def keysym_from_name(self, name: str) -> int:
"""Get the keysym for a key from its name"""
return xkb.keysym_from_name(name, case_insensitive=True)
@ -745,8 +839,34 @@ class Core(base.Core, wlrq.HasListeners):
mods = wlrq.translate_masks(modifiers)
if (keysym, mods) in self.grabbed_keys:
assert self.qtile is not None
self.qtile.process_key_event(keysym, mods)
return
if self.focused_internal:
self.focused_internal.process_key_press(keysym)
def cmd_set_keymap(
self,
layout: Optional[str] = None,
options: Optional[str] = None,
variant: Optional[str] = None,
) -> None:
"""
Set the keymap for the current keyboard.
The options correspond to xkbcommon configuration environmental variables and if
not specified are taken from the environment. Acceptable values are strings
identical to those accepted by the env variables.
"""
if self.keyboards:
self.keyboards[-1].set_keymap(layout, options, variant)
else:
logger.warning("Could not set keymap: no keyboards set up.")
def cmd_change_vt(self, vt: int) -> bool:
"""Change virtual terminal to that specified"""
success = self.backend.get_session().change_vt(vt)
if not success:
logger.warning(f"Could not change VT to: {vt}")
return success

View File

@ -19,14 +19,13 @@ class Drawer(base.Drawer):
A helper class for drawing and text layout.
1. We stage drawing operations locally in memory using a cairo RecordingSurface.
2. Then apply these operations to our ImageSurface self._source
3. Then copy the pixels onto the wlr_texture self._target
2. Then apply these operations to our ImageSurface self._source.
3. Then copy the pixels onto the window's wlr_texture.
"""
def __init__(self, qtile: Qtile, win: Internal, width: int, height: int):
base.Drawer.__init__(self, qtile, win, width, height)
self._target = win.texture
self._stride = cairocffi.ImageSurface.format_stride_for_width(
cairocffi.FORMAT_ARGB32, self.width
)
@ -36,7 +35,7 @@ class Drawer(base.Drawer):
context.set_source_rgba(*utils.rgb("#000000"))
context.paint()
def draw(
def _draw(
self,
offsetx: int = 0,
offsety: int = 0,
@ -72,22 +71,16 @@ class Drawer(base.Drawer):
context.paint()
# Copy drawn ImageSurface data into rendered wlr_texture
self._target.write_pixels(
self._win.texture.write_pixels( # type: ignore
self._stride,
width, # type: ignore
height, # type: ignore
width,
height,
cairocffi.cairo.cairo_image_surface_get_data(self._source._pointer),
dst_x=offsetx,
dst_y=offsety,
)
self._win.damage() # type: ignore
# If the widget is not being reflected then clear RecordingSurface of operations
# If it is, we need to keep the RecordingSurface contents until the mirrors have
# been drawn
if not self.mirrors:
self._reset_surface()
def clear(self, colour):
# Draw background straight to ImageSurface
ctx = cairocffi.Context(self._source)

View File

@ -55,7 +55,7 @@ class Keyboard(HasListeners):
self.keyboard.set_repeat_info(25, 600)
self.xkb_context = xkb.Context()
self._keymaps: Dict[Tuple[Optional[str], Optional[str]], xkb.Keymap] = {}
self._keymaps: Dict[Tuple[Optional[str], ...], xkb.Keymap] = {}
self.set_keymap(None, None, None)
self.add_listener(self.keyboard.modifiers_event, self._on_modifier)
@ -72,12 +72,10 @@ class Keyboard(HasListeners):
self, layout: Optional[str], options: Optional[str], variant: Optional[str]
) -> None:
"""
Set the keymap for this keyboard. `layout` and `options` correspond to
XKB_DEFAULT_LAYOUT and XKB_DEFAULT_OPTIONS and if not specified are taken from
the environment.
Set the keymap for this keyboard.
"""
if (layout, options) in self._keymaps:
keymap = self._keymaps[(layout, options)]
if (layout, options, variant) in self._keymaps:
keymap = self._keymaps[(layout, options, variant)]
else:
keymap = self.xkb_context.keymap_new_from_names(
layout=layout, options=options, variant=variant
@ -90,6 +88,7 @@ class Keyboard(HasListeners):
self.finalize()
def _on_modifier(self, _listener, _data):
self.seat.set_keyboard(self.device)
self.seat.keyboard_notify_modifiers(self.keyboard.modifiers)
def _on_key(self, _listener, event: KeyboardKeyEvent):

View File

@ -23,9 +23,10 @@ from __future__ import annotations
import os
import typing
from wlroots.util.box import Box
from wlroots.util.clock import Timespec
from wlroots.util.region import PixmanRegion32
from wlroots.wlr_types import Box, Matrix
from wlroots.wlr_types import Matrix
from wlroots.wlr_types import Output as wlrOutput
from wlroots.wlr_types import OutputDamage
from wlroots.wlr_types.layer_shell_v1 import (
@ -39,12 +40,13 @@ from libqtile.backend.wayland.wlrq import HasListeners
from libqtile.log_utils import logger
if typing.TYPE_CHECKING:
from typing import List, Tuple
from typing import List, Tuple, Union
from wlroots.wlr_types import Surface
from libqtile.backend.wayland.core import Core
from libqtile.backend.wayland.window import WindowType
from libqtile.backend.wayland.wlrq import Dnd
from libqtile.config import Screen
@ -73,7 +75,7 @@ class Output(HasListeners):
# First test output
wlr_output.set_custom_mode(800, 600, 0)
else:
# Secound test output
# Second test output
wlr_output.set_custom_mode(640, 480, 0)
wlr_output.commit()
@ -84,11 +86,13 @@ class Output(HasListeners):
@property
def screen(self) -> Screen:
assert self.core.qtile is not None
x, y, w, h = self.get_geometry()
for screen in self.core.qtile.screens:
if screen.x == x and screen.y == y:
if screen.width == w and screen.height == h:
return screen
if len(self.core.qtile.screens) > 1:
x, y, w, h = self.get_geometry()
for screen in self.core.qtile.screens:
if screen.x == x and screen.y == y:
if screen.width == w and screen.height == h:
return screen
return self.core.qtile.current_screen
def _on_destroy(self, _listener, _data):
@ -111,10 +115,16 @@ class Output(HasListeners):
now = Timespec.get_monotonic_time()
renderer = self.renderer
renderer.begin(*wlr_output.effective_resolution())
renderer.begin(wlr_output._ptr.width, wlr_output._ptr.height)
scale = wlr_output.scale
if self.wallpaper:
renderer.render_texture(self.wallpaper, self.transform_matrix, 0, 0, 1)
width, height = wlr_output.effective_resolution()
box = Box(0, 0, int(width * scale), int(height * scale))
matrix = Matrix.project_box(
box, wlr_output.transform, 0, wlr_output.transform_matrix
)
renderer.render_texture_with_matrix(self.wallpaper, matrix, 1)
else:
renderer.clear([0, 0, 0, 1])
@ -126,13 +136,16 @@ class Output(HasListeners):
for window in mapped:
if isinstance(window, Internal):
renderer.render_texture(
window.texture,
self.transform_matrix,
window.x - self.x, # layout coordinates -> output coordinates
window.y - self.y,
window.opacity,
box = Box(
int((window.x - self.x) * scale),
int((window.y - self.y) * scale),
int(window.width * scale),
int(window.height * scale)
)
matrix = Matrix.project_box(
box, wlr_output.transform, 0, wlr_output.transform_matrix
)
renderer.render_texture_with_matrix(window.texture, matrix, window.opacity)
else:
rdata = (
now,
@ -144,6 +157,9 @@ class Output(HasListeners):
)
window.surface.for_each_surface(self._render_surface, rdata)
if self.core.live_dnd:
self._render_dnd_icon(now)
wlr_output.render_software_cursors(damage=damage)
renderer.end()
@ -203,6 +219,26 @@ class Output(HasListeners):
self.renderer.render_texture_with_matrix(texture, matrix, opacity)
surface.send_frame_done(now)
def _render_dnd_icon(self, now: Timespec) -> None:
"""Render the drag-n-drop icon if there is one."""
dnd = self.core.live_dnd
assert dnd
icon = dnd.wlr_drag.icon
if icon.mapped:
texture = icon.surface.get_texture()
if texture:
scale = self.wlr_output.scale
box = Box(
int((dnd.x - self.x) * scale),
int((dnd.y - self.y) * scale),
int(icon.surface.current.width * scale),
int(icon.surface.current.height * scale),
)
inverse = wlrOutput.transform_invert(icon.surface.current.transform)
matrix = Matrix.project_box(box, inverse, 0, self.transform_matrix)
self.renderer.render_texture_with_matrix(texture, matrix, 1)
icon.surface.send_frame_done(now)
def get_geometry(self) -> Tuple[int, int, int, int]:
width, height = self.wlr_output.effective_resolution()
return int(self.x), int(self.y), width, height
@ -284,17 +320,17 @@ class Output(HasListeners):
self.core.stack_windows()
def contains(self, win: WindowType) -> bool:
def contains(self, rect: Union[WindowType, Dnd]) -> bool:
"""Returns whether the given window is visible on this output."""
if win.x + win.width < self.x:
if rect.x + rect.width < self.x:
return False
if win.y + win.height < self.y:
if rect.y + rect.height < self.y:
return False
ow, oh = self.wlr_output.effective_resolution()
if self.x + ow < win.x:
if self.x + ow < rect.x:
return False
if self.y + oh < win.y:
if self.y + oh < rect.y:
return False
return True

View File

@ -25,9 +25,11 @@ import typing
import cairocffi
import pywayland
import wlroots.wlr_types.foreign_toplevel_management_v1 as ftm
from wlroots import ffi
from wlroots.util.box import Box
from wlroots.util.edges import Edges
from wlroots.wlr_types import Box, Texture
from wlroots.wlr_types import Texture
from wlroots.wlr_types.layer_shell_v1 import LayerSurfaceV1
from wlroots.wlr_types.xdg_shell import (
XdgPopup,
@ -35,7 +37,7 @@ from wlroots.wlr_types.xdg_shell import (
XdgTopLevelSetFullscreenEvent,
)
from libqtile import hook, utils
from libqtile import config, hook, utils
from libqtile.backend import base
from libqtile.backend.base import FloatStates
from libqtile.backend.wayland.drawer import Drawer
@ -53,7 +55,7 @@ if typing.TYPE_CHECKING:
from libqtile.command.base import ItemT
from libqtile.core.manager import Qtile
from libqtile.group import _Group
from libqtile.utils import ColorType
from libqtile.utils import ColorsType, ColorType
EDGES_TILED = Edges.TOP | Edges.BOTTOM | Edges.LEFT | Edges.RIGHT
EDGES_FLOAT = Edges.NONE
@ -84,7 +86,7 @@ class Window(base.Window, HasListeners):
self.x = 0
self.y = 0
self.bordercolor: List[ffi.CData] = [_rgb((0, 0, 0, 1))]
self.opacity: float = 1.0
self._opacity: float = 1.0
self._outputs: List[Output] = []
# These become non-zero when being mapping for the first time
@ -92,10 +94,9 @@ class Window(base.Window, HasListeners):
self._height: int = 0
assert isinstance(surface, XdgSurface)
if surface.toplevel.title:
self.name = surface.toplevel.title
self._app_id: Optional[str] = surface.toplevel.app_id
surface.set_tiled(EDGES_TILED)
self.ftm_handle = core.foreign_toplevel_manager_v1.create_handle()
surface.data = self.ftm_handle
self._float_state = FloatStates.NOT_FLOATING
self.float_x: Optional[int] = None
@ -107,9 +108,6 @@ class Window(base.Window, HasListeners):
self.add_listener(surface.unmap_event, self._on_unmap)
self.add_listener(surface.destroy_event, self._on_destroy)
self.add_listener(surface.new_popup_event, self._on_new_popup)
self.add_listener(surface.toplevel.request_fullscreen_event, self._on_request_fullscreen)
self.add_listener(surface.toplevel.set_title_event, self._on_set_title)
self.add_listener(surface.toplevel.set_app_id_event, self._on_set_app_id)
self.add_listener(surface.surface.commit_event, self._on_commit)
self.add_listener(surface.surface.new_subsurface_event, self._on_new_subsurface)
@ -118,6 +116,12 @@ class Window(base.Window, HasListeners):
for subsurface in self.subsurfaces:
subsurface.finalize()
for pc in self.core.pointer_constraints.copy():
if pc.window is self:
pc.finalize()
self.ftm_handle.destroy()
@property
def wid(self):
return self._wid
@ -171,10 +175,41 @@ class Window(base.Window, HasListeners):
logger.debug(f"Managing new top-level window with window ID: {self.wid}")
# Save the client's desired geometry
geometry = self.surface.get_geometry()
surface = self.surface
geometry = surface.get_geometry()
self._width = self._float_width = geometry.width
self._height = self._float_height = geometry.height
# Tell the client to render tiled edges
surface.set_tiled(EDGES_TILED)
# Get the client's name
if surface.toplevel.title:
self.name = surface.toplevel.title
self.ftm_handle.set_title(self.name)
if self._app_id:
self.ftm_handle.set_app_id(self._app_id)
# Add the toplevel's listeners
self.add_listener(surface.toplevel.request_fullscreen_event, self._on_request_fullscreen)
self.add_listener(surface.toplevel.set_title_event, self._on_set_title)
self.add_listener(surface.toplevel.set_app_id_event, self._on_set_app_id)
self.add_listener(
self.ftm_handle.request_maximize_event, self._on_foreign_request_maximize
)
self.add_listener(
self.ftm_handle.request_minimize_event, self._on_foreign_request_minimize
)
self.add_listener(
self.ftm_handle.request_activate_event, self._on_foreign_request_activate
)
self.add_listener(
self.ftm_handle.request_fullscreen_event, self._on_foreign_request_fullscreen
)
self.add_listener(
self.ftm_handle.request_close_event, self._on_foreign_request_close
)
self.qtile.manage(self)
if self.group.screen:
@ -195,7 +230,11 @@ class Window(base.Window, HasListeners):
if self.mapped:
logger.warning("Window destroyed before unmap event.")
self.mapped = False
self.qtile.unmanage(self.wid)
# Don't try to unmanage if we were never managed.
if self not in self.core.pending_windows:
self.qtile.unmanage(self.wid)
self.finalize()
def _on_new_popup(self, _listener, xdg_popup: XdgPopup):
@ -210,11 +249,13 @@ class Window(base.Window, HasListeners):
def _on_set_title(self, _listener, _data):
logger.debug("Signal: window set_title")
self.name = self.surface.toplevel.title
self.ftm_handle.set_title(self.name)
hook.fire('client_name_updated', self)
def _on_set_app_id(self, _listener, _data):
logger.debug("Signal: window set_app_id")
self._app_id = self.surface.toplevel.app_id
self.ftm_handle.set_app_id(self._app_id)
def _on_commit(self, _listener, _data):
self.damage()
@ -222,6 +263,36 @@ class Window(base.Window, HasListeners):
def _on_new_subsurface(self, _listener, subsurface: WlrSubSurface):
self.subsurfaces.append(SubSurface(self, subsurface))
def _on_foreign_request_maximize(
self, _listener, event: ftm.ForeignToplevelHandleV1MaximizedEvent
):
logger.debug("Signal: foreign_toplevel_management request_maximize")
self.maximized = event.maximized
def _on_foreign_request_minimize(
self, _listener, event: ftm.ForeignToplevelHandleV1MinimizedEvent
):
logger.debug("Signal: foreign_toplevel_management request_minimize")
self.minimized = event.minimized
def _on_foreign_request_fullscreen(
self, _listener, event: ftm.ForeignToplevelHandleV1FullscreenEvent
):
logger.debug("Signal: foreign_toplevel_management request_fullscreen")
self.fullscreen = event.fullscreen
def _on_foreign_request_activate(
self, _listener, event: ftm.ForeignToplevelHandleV1ActivatedEvent
):
logger.debug("Signal: foreign_toplevel_management request_activate")
if self.group:
self.qtile.current_screen.set_group(self.group)
self.group.focus(self)
def _on_foreign_request_close(self, _listener, _event):
logger.debug("Signal: foreign_toplevel_management request_close")
self.kill()
def has_fixed_size(self) -> bool:
assert isinstance(self.surface, XdgSurface)
state = self.surface.toplevel._ptr.current
@ -297,7 +368,7 @@ class Window(base.Window, HasListeners):
if switch_group:
group.cmd_toscreen(toggle=False)
def paint_borders(self, color: Union[ColorType, List[ColorType]], width) -> None:
def paint_borders(self, color: ColorsType, width) -> None:
if color:
if isinstance(color, list):
if len(color) > width:
@ -347,18 +418,19 @@ class Window(base.Window, HasListeners):
if do_full:
screen = self.group.screen or \
self.qtile.find_closest_screen(self.x, self.y)
bw = self.group.floating_layout.fullscreen_border_width
self._enablefloating(
screen.x,
screen.y,
screen.width,
screen.height,
screen.width - 2 * bw,
screen.height - 2 * bw,
new_float_state=FloatStates.FULLSCREEN
)
return
if self._float_state == FloatStates.FULLSCREEN:
elif self._float_state == FloatStates.FULLSCREEN:
self.floating = False
self.ftm_handle.set_fullscreen(do_full)
@property
def maximized(self):
return self._float_state == FloatStates.MAXIMIZED
@ -369,17 +441,20 @@ class Window(base.Window, HasListeners):
screen = self.group.screen or \
self.qtile.find_closest_screen(self.x, self.y)
bw = self.group.floating_layout.max_border_width
self._enablefloating(
screen.dx,
screen.dy,
screen.dwidth,
screen.dheight,
screen.dwidth - 2 * bw,
screen.dheight - 2 * bw,
new_float_state=FloatStates.MAXIMIZED
)
else:
if self._float_state == FloatStates.MAXIMIZED:
self.floating = False
self.ftm_handle.set_maximized(do_maximize)
@property
def minimized(self):
return self._float_state == FloatStates.MINIMIZED
@ -393,11 +468,10 @@ class Window(base.Window, HasListeners):
if self._float_state == FloatStates.MINIMIZED:
self.floating = False
self.ftm_handle.set_minimized(do_minimize)
def focus(self, warp: bool) -> None:
self.core.focus_window(self)
if isinstance(self, base.Internal):
# self.core.focus_window is enough for internal windows
return
if warp and self.qtile.config.cursor_warp:
self.core.warp_pointer(
@ -407,6 +481,7 @@ class Window(base.Window, HasListeners):
if self.group:
self.group.current_window = self
hook.fire("client_focus", self)
def place(self, x, y, width, height, borderwidth, bordercolor,
@ -521,13 +596,19 @@ class Window(base.Window, HasListeners):
fullscreen=self._float_state == FloatStates.FULLSCREEN
)
def match(self, match: config.Match) -> bool:
return match.compare(self)
def _items(self, name: str) -> ItemT:
if name == "group":
return True, []
elif name == "layout":
return True, list(range(len(self.group.layouts)))
elif name == "screen" and self.group.screen is not None:
return True, []
if name == "layout":
if self.group:
return True, list(range(len(self.group.layouts)))
return None
if name == "screen":
if self.group and self.group.screen:
return True, []
return None
def _select(self, name, sel):
@ -554,6 +635,29 @@ class Window(base.Window, HasListeners):
def cmd_set_position_floating(self, x: int, y: int) -> None:
self._tweak_float(x=x, y=y)
def cmd_set_position(self, x: int, y: int) -> None:
if self.floating:
self._tweak_float(x=x, y=y)
return
if self.group:
cx = self.core.cursor.x
cy = self.core.cursor.y
for window in self.group.windows:
if (
window is not self and
not window.floating and
window.x <= cx <= (window.x + window.width) and
window.y <= cy <= (window.y + window.height)
):
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.layout_all()
return
def cmd_set_size_floating(self, w: int, h: int) -> None:
self._tweak_float(w=w, h=h)
@ -656,12 +760,13 @@ class Internal(base.Internal, Window):
self._wid: int = self.core.new_wid()
self.x: int = x
self.y: int = y
self.opacity: float = 1.0
self._opacity: float = 1.0
self._width: int = width
self._height: int = height
self._outputs: List[Output] = []
self._find_outputs()
self._reset_texture()
self._group = None
def finalize(self):
self.hide()
@ -709,9 +814,15 @@ class Internal(base.Internal, Window):
self.mapped = True
self.damage()
def focus(self, warp: bool) -> None:
self.core.focus_window(self)
def kill(self) -> None:
self.hide()
del self.qtile.windows_map[self.wid]
if self.wid in self.qtile.windows_map:
# It will be present during config reloads; absent during shutdown as this
# will follow graceful_shutdown
del self.qtile.windows_map[self.wid]
def place(self, x, y, width, height, borderwidth, bordercolor,
above=False, margin=None, respect_hints=False):
@ -792,7 +903,22 @@ class Static(base.Static, Window):
self._app_id = surface.toplevel.app_id
self.add_listener(surface.toplevel.set_title_event, self._on_set_title)
self.add_listener(surface.toplevel.set_app_id_event, self._on_set_app_id)
self.ftm_handle = surface.data
assert self.ftm_handle
self.add_listener(
self.ftm_handle.request_close_event, self._on_foreign_request_close
)
self._find_outputs()
self.screen = qtile.current_screen
def finalize(self):
self.finalize_listeners()
for subsurface in self.subsurfaces:
subsurface.finalize()
for pc in self.core.pointer_constraints.copy():
if pc.window is self:
pc.finalize()
@property
def mapped(self) -> bool:
@ -830,7 +956,7 @@ class Static(base.Static, Window):
self.mapped = True
if self.is_layer:
self.output.organise_layers()
self.core.focus_window(self, self.surface.surface)
self.focus(True)
def _on_unmap(self, _listener, data):
logger.debug("Signal: window unmap")
@ -848,6 +974,17 @@ class Static(base.Static, Window):
def has_fixed_size(self) -> bool:
return False
def focus(self, warp: bool) -> None:
self.core.focus_window(self)
if warp and self.qtile.config.cursor_warp:
self.core.warp_pointer(
self.x + self.width // 2,
self.y + self.height // 2,
)
hook.fire("client_focus", self)
def kill(self):
if self.is_layer:
self.surface.close()

View File

@ -28,14 +28,25 @@ import cairocffi
from pywayland.server import Listener
from wlroots.wlr_types import Texture
from wlroots.wlr_types.keyboard import KeyboardModifier
from wlroots.wlr_types.pointer_constraints_v1 import (
PointerConstraintV1,
PointerConstraintV1StateField,
)
from wlroots.wlr_types.xdg_shell import XdgSurface
from libqtile.backend.base import Internal
from libqtile.log_utils import logger
from libqtile.utils import QtileError
if TYPE_CHECKING:
from typing import Callable, List
from typing import Callable, List, Optional, Set
from pywayland.server import Signal
from wlroots.wlr_types import Box, data_device_manager
from libqtile.backend.wayland.core import Core
from libqtile.backend.wayland.output import Output
from libqtile.backend.wayland.window import WindowType
class WlrQError(QtileError):
@ -168,3 +179,130 @@ class HasListeners:
def finalize_listeners(self):
for listener in reversed(self._listeners):
listener.remove()
class PointerConstraint(HasListeners):
"""
A small object to listen to signals on `struct wlr_pointer_constraint_v1` instances.
"""
rect: Box
def __init__(self, core: Core, wlr_constraint: PointerConstraintV1):
self.core = core
self.wlr_constraint = wlr_constraint
self.window: Optional[WindowType] = None
self._warp_target = (0, 0)
self._needs_warp = False
self.add_listener(wlr_constraint.set_region_event, self._on_set_region)
self.add_listener(wlr_constraint.destroy_event, self._on_destroy)
self._get_window()
def _get_window(self):
for win in self.core.qtile.windows_map.values():
if not isinstance(win, Internal) and isinstance(win.surface, XdgSurface):
if win.surface.surface == self.wlr_constraint.surface:
break
else:
self.finalize()
self.window = win
def finalize(self):
if self.core.active_pointer_constraint is self:
self.disable()
self.finalize_listeners()
self.core.pointer_constraints.remove(self)
def _on_set_region(self, _listener, _data):
logger.debug("Signal: wlr_pointer_constraint_v1 set_region")
self._get_region()
def _on_destroy(self, _listener, wlr_constraint: PointerConstraintV1):
logger.debug("Signal: wlr_pointer_constraint_v1 destroy")
self.finalize()
def _on_commit(self, _listener, _data):
if self._needs_warp:
# Warp in case the pointer is not inside the rect
if not self.rect.contains_point(self.cursor.x, self.cursor.y):
self.core.warp_pointer(*self._warp_target)
self._needs_warp = False
def _get_region(self):
rect = self.wlr_constraint.region.rectangles_as_boxes()[0]
rect.x += self.window.x + self.window.borderwidth
rect.y += self.window.y + self.window.borderwidth
self._warp_target = (rect.x + rect.width / 2, rect.y + rect.height / 2)
self.rect = rect
self._needs_warp = True
def enable(self):
logger.debug("Enabling pointer constraints.")
self.core.active_pointer_constraint = self
self._get_region()
self.add_listener(self.wlr_constraint.surface.commit_event, self._on_commit)
self.wlr_constraint.send_activated()
def disable(self):
logger.debug("Disabling pointer constraints.")
if self.wlr_constraint.current.committed & PointerConstraintV1StateField.CURSOR_HINT:
x, y = self.wlr_constraint.current.cursor_hint
self.core.warp_pointer(x + self.window.x, y + self.window.y)
self.core.active_pointer_constraint = None
self.wlr_constraint.send_deactivated()
class Dnd(HasListeners):
"""A helper for drag and drop functionality."""
def __init__(self, core: Core, wlr_drag: data_device_manager.Drag):
self.core = core
self.wlr_drag = wlr_drag
self._outputs: Set[Output] = set()
self.x: float = core.cursor.x
self.y: float = core.cursor.y
self.width: int = 0 # Set upon surface commit
self.height: int = 0
self.add_listener(wlr_drag.destroy_event, self._on_destroy)
self.add_listener(wlr_drag.icon.map_event, self._on_icon_map)
self.add_listener(wlr_drag.icon.unmap_event, self._on_icon_unmap)
self.add_listener(wlr_drag.icon.destroy_event, self._on_icon_destroy)
self.add_listener(wlr_drag.icon.surface.commit_event, self._on_icon_commit)
def finalize(self) -> None:
self.finalize_listeners()
self.core.live_dnd = None
def _on_destroy(self, _listener, _event) -> None:
logger.debug("Signal: wlr_drag destroy")
self.finalize()
def _on_icon_map(self, _listener, _event) -> None:
logger.debug("Signal: wlr_drag_icon map")
for output in self._outputs:
output.damage()
def _on_icon_unmap(self, _listener, _event) -> None:
logger.debug("Signal: wlr_drag_icon unmap")
for output in self._outputs:
output.damage()
def _on_icon_destroy(self, _listener, _event) -> None:
logger.debug("Signal: wlr_drag_icon destroy")
def _on_icon_commit(self, _listener, _event) -> None:
self.width = self.wlr_drag.icon.surface.current.width
self.height = self.wlr_drag.icon.surface.current.height
self.position(self.core.cursor.x, self.core.cursor.y)
def position(self, cx: float, cy: float) -> None:
self.x = cx
self.y = cy
self._outputs = {o for o in self.core.outputs if o.contains(self)}
for output in self._outputs:
output.damage()

View File

@ -53,7 +53,6 @@ if TYPE_CHECKING:
_IGNORED_EVENTS = {
xcffib.xproto.CreateNotifyEvent,
xcffib.xproto.FocusInEvent,
xcffib.xproto.FocusOutEvent,
xcffib.xproto.KeyReleaseEvent,
# DWM handles this to help "broken focusing windows".
xcffib.xproto.MapNotifyEvent,
@ -106,15 +105,15 @@ class Core(base.Core):
logger.error("not starting; existing window manager {}".format(existing_wmname))
raise ExistingWMException(existing_wmname)
self._root.set_attribute(
eventmask=(
EventMask.StructureNotify
| EventMask.SubstructureNotify
| EventMask.SubstructureRedirect
| EventMask.EnterWindow
| EventMask.LeaveWindow
)
self.eventmask = (
EventMask.StructureNotify
| EventMask.SubstructureNotify
| EventMask.SubstructureRedirect
| EventMask.EnterWindow
| EventMask.LeaveWindow
| EventMask.ButtonPress
)
self._root.set_attribute(eventmask=self.eventmask)
self._root.set_property(
"_NET_SUPPORTED", [self.conn.atoms[x] for x in xcbq.SUPPORTED_ATOMS]
@ -223,10 +222,18 @@ class Core(base.Core):
loop.remove_reader(self.fd)
self.fd = None
def scan(self) -> None:
"""Scan for existing windows"""
def distribute_windows(self, initial) -> None:
"""Assign windows to groups"""
assert self.qtile is not None
if not initial:
# We are just reloading config
for win in self.qtile.windows_map.values():
if type(win) is window.Window:
win.set_group()
return
# Qtile just started - scan for clients
_, _, children = self._root.query_tree()
for item in children:
try:
@ -242,8 +249,8 @@ class Core(base.Core):
item.unmap()
continue
win = self.qtile.windows_map.get(item.wid)
if win:
if item.wid in self.qtile.windows_map:
win = self.qtile.windows_map[item.wid]
win.unhide()
return
@ -291,10 +298,9 @@ class Core(base.Core):
if event_type.endswith("Event"):
event_type = event_type[:-5]
logger.debug(event_type)
for target in self._get_target_chain(event_type, event):
logger.debug("Handling: {event_type}".format(event_type=event_type))
targets = self._get_target_chain(event_type, event)
logger.debug(f"X11 event: {event_type} (targets: {len(targets)})")
for target in targets:
ret = target(event)
if not ret:
break
@ -368,9 +374,6 @@ class Core(base.Core):
if hasattr(self, handler):
chain.append(getattr(self, handler))
if not chain:
logger.info("Unhandled event: {event_type}".format(event_type=event_type))
return chain
def get_valid_timestamp(self):
@ -418,8 +421,9 @@ class Core(base.Core):
This is needed for third party tasklists and drag and drop of tabs in
chrome
"""
# Regular top-level managed windows, i.e. excluding Static, Internal and Systray Icons
wids = [
wid for wid, c in windows_map.items() if not isinstance(c, window.Internal)
wid for wid, c in windows_map.items() if isinstance(c, window.Window)
]
self._root.set_property("_NET_CLIENT_LIST", wids)
# TODO: check stack order
@ -452,11 +456,7 @@ class Core(base.Core):
for code in codes:
if code == 0:
logger.warning(
"Keysym could not be mapped: {keysym}, mask: {modmask}".format(
keysym=hex(keysym), modmask=modmask
)
)
logger.warning(f"Can't grab {key} (unknown keysym: {hex(keysym)})")
continue
for amask in self._auto_modmasks():
self.conn.conn.core.GrabKey(
@ -554,6 +554,10 @@ class Core(base.Core):
self.qtile.manage(internal)
return internal
def handle_FocusOut(self, event) -> None: # noqa: N802
if event.detail == xcffib.xproto.NotifyDetail._None:
self.conn.fixup_focus()
def handle_SelectionNotify(self, event) -> None: # noqa: N802
if not getattr(event, "owner", None):
return
@ -594,7 +598,7 @@ class Core(base.Core):
try:
self.qtile.groups[index].cmd_toscreen()
except IndexError:
logger.info("Invalid Desktop Index: %s" % index)
logger.debug("Invalid desktop index: %s" % index)
def handle_KeyPress(self, event) -> None: # noqa: N802
assert self.qtile is not None
@ -698,25 +702,40 @@ class Core(base.Core):
def handle_UnmapNotify(self, event) -> None: # noqa: N802
assert self.qtile is not None
if event.event != self._root.wid:
win = self.qtile.windows_map.get(event.window)
if win and getattr(win, "group", None):
try:
win.hide()
assert isinstance(win, window._Window)
win.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.qtile.unmanage(event.window)
if self.qtile.current_window is None:
self.conn.fixup_focus()
win = self.qtile.windows_map.get(event.window)
if win and getattr(win, "group", None):
try:
win.hide()
win.state = window.WithdrawnState # type: ignore
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
# Clear these atoms as per spec
win.window.conn.conn.core.DeleteProperty( # type: ignore
win.wid, win.window.conn.atoms["_NET_WM_STATE"] # type: ignore
)
win.window.conn.conn.core.DeleteProperty( # type: ignore
win.wid, win.window.conn.atoms["_NET_WM_DESKTOP"] # type: ignore
)
self.qtile.unmanage(event.window)
if self.qtile.current_window is None:
self.conn.fixup_focus()
def handle_ScreenChangeNotify(self, event) -> None: # noqa: N802
hook.fire("screen_change", event)
hook.fire("screens_reconfigured")
@contextlib.contextmanager
def disable_unmap_events(self):
self._root.set_attribute(
eventmask=self.eventmask & (~EventMask.SubstructureNotify)
)
yield
self._root.set_attribute(eventmask=self.eventmask)
@property
def painter(self):
@ -743,7 +762,7 @@ class Core(base.Core):
Parameters
==========
e : xcb event
e: xcb event
Click event used to determine window to focus
"""
qtile = self.qtile

View File

@ -130,15 +130,9 @@ class Drawer(base.Drawer):
ctx.set_source_surface(self.surface, 0, 0)
ctx.paint()
# If the widget is not being reflected then clear RecordingSurface of operations
# If it is, we need to keep the RecordingSurface contents until the mirrors have
# been drawn
if not self.mirrors:
self._reset_surface()
self.previous_rect = self.current_rect
def draw(
def _draw(
self,
offsetx: int = 0,
offsety: int = 0,

View File

@ -89,6 +89,7 @@ def _geometry_getter(attr):
self.y = g.y
self.width = g.width
self.height = g.height
self.depth = g.depth
return getattr(self, "_" + attr)
return get_attr
@ -312,9 +313,9 @@ class XWindow:
"""
Parameters
==========
name : String Atom name
type : String Atom name
format : 8, 16, 32
name: String Atom name
type: String Atom name
format: 8, 16, 32
"""
if name in xcbq.PropertyMap:
if type or format:
@ -427,10 +428,12 @@ class XWindow:
parent = XWindow(self.conn, q.parent)
return root, parent, [XWindow(self.conn, i) for i in q.children]
def paint_borders(self, colors, borderwidth, width, height):
def paint_borders(self, depth, colors, borderwidth, width, height):
"""
This method is used only by the managing Window class.
"""
self.set_property('_NET_FRAME_EXTENTS', [borderwidth] * 4)
if not colors or not borderwidth:
return
@ -447,7 +450,7 @@ class XWindow:
with PixmapID(self.conn.conn) as pixmap:
with GContextID(self.conn.conn) as gc:
core.CreatePixmap(
self.conn.default_screen.root_depth, pixmap, self.wid, outer_w, outer_h
depth, pixmap, self.wid, outer_w, outer_h
)
core.CreateGC(gc, pixmap, 0, None)
borders = len(colors)
@ -464,15 +467,15 @@ class XWindow:
)
core.PolyFillRectangle(pixmap, gc, 1, [rect])
coord += borderwidths[i]
self._set_borderpixmap(pixmap, gc, borderwidth, width, height)
self._set_borderpixmap(depth, pixmap, gc, borderwidth, width, height)
def _set_borderpixmap(self, pixmap, gc, borderwidth, width, height):
def _set_borderpixmap(self, depth, pixmap, gc, borderwidth, width, height):
core = self.conn.conn.core
outer_w = width + borderwidth * 2
outer_h = height + borderwidth * 2
with PixmapID(self.conn.conn) as border:
core.CreatePixmap(
self.conn.default_screen.root_depth, border, self.wid, outer_w, outer_h
depth, border, self.wid, outer_w, outer_h
)
most_w = outer_w - borderwidth
most_h = outer_h - borderwidth
@ -500,6 +503,7 @@ class _Window:
self._y = g.y
self._width = g.width
self._height = g.height
self._depth = g.depth
except xcffib.xproto.DrawableError:
# Whoops, we were too early, so let's ignore it for now and get the
# values on demand.
@ -507,6 +511,7 @@ class _Window:
self._y = None
self._width = None
self._height = None
self._depth = None
self.float_x: Optional[int] = None
self.float_y: Optional[int] = None
@ -545,6 +550,10 @@ class _Window:
fset=_geometry_setter("height"),
fget=_geometry_getter("height"),
)
depth = property(
fset=_geometry_setter("depth"),
fget=_geometry_getter("depth"),
)
@property
def wid(self):
@ -643,7 +652,6 @@ class _Window:
state = self.window.get_net_wm_state()
logger.debug('_NET_WM_STATE: %s', state)
for s in triggered:
setattr(self, s, (s in state))
@ -695,6 +703,7 @@ class _Window:
@property
def opacity(self):
assert hasattr(self, "window")
opacity = self.window.get_property(
"_NET_WM_WINDOW_OPACITY", unpack=int
)
@ -707,12 +716,11 @@ class _Window:
return as_float
@opacity.setter
def opacity(self, opacity):
def opacity(self, opacity: float) -> None:
if 0.0 <= opacity <= 1.0:
real_opacity = int(opacity * 0xffffffff)
assert hasattr(self, "window")
self.window.set_property('_NET_WM_WINDOW_OPACITY', real_opacity)
else:
return
def kill(self):
if "WM_DELETE_WINDOW" in self.window.get_wm_protocols():
@ -741,7 +749,8 @@ class _Window:
def hide(self):
# We don't want to get the UnmapNotify for this unmap
with self.disable_mask(EventMask.StructureNotify):
self.window.unmap()
with self.qtile.core.disable_unmap_events():
self.window.unmap()
self.hidden = True
def unhide(self):
@ -765,6 +774,29 @@ class _Window:
eventmask=self._window_mask
)
def _grab_click(self):
# Grab button 1 to focus upon click when unfocussed
for amask in self.qtile.core._auto_modmasks():
self.qtile.core.conn.conn.core.GrabButton(
True,
self.window.wid,
EventMask.ButtonPress,
xcffib.xproto.GrabMode.Sync,
xcffib.xproto.GrabMode.Async,
xcffib.xproto.Atom._None,
xcffib.xproto.Atom._None,
1,
amask,
)
def _ungrab_click(self):
# Ungrab button 1 when focussed
self.qtile.core.conn.conn.core.UngrabButton(
xcffib.xproto.Atom.Any,
self.window.wid,
xcffib.xproto.ModMask.Any,
)
def get_pid(self):
return self.window.get_net_wm_pid()
@ -775,16 +807,16 @@ class _Window:
Parameters
==========
x : int
y : int
width : int
height : int
borderwidth : int
bordercolor : string
above : bool, optional
margin : int or list, optional
x: int
y: int
width: int
height: int
borderwidth: int
bordercolor: string
above: bool, optional
margin: int or list, optional
space around window as int or list of ints [N E S W]
above : bool, optional
above: bool, optional
If True, the geometry will be adjusted to respect hints provided by the
client.
"""
@ -870,7 +902,7 @@ class _Window:
self.borderwidth = width
self.bordercolor = color
self.window.configure(borderwidth=width)
self.window.paint_borders(color, width, self.width, self.height)
self.window.paint_borders(self.depth, color, width, self.width, self.height)
def send_configure_notify(self, x, y, width, height):
"""Send a synthetic ConfigureNotify"""
@ -968,7 +1000,14 @@ class _Window:
state.remove(atom)
self.window.set_property('_NET_WM_STATE', state)
# re-grab button events on the previously focussed window
old = self.qtile.core._root.get_property("_NET_ACTIVE_WINDOW", 'WINDOW', unpack=int)
if old and old[0] in self.qtile.windows_map:
old_win = self.qtile.windows_map[old[0]]
if not isinstance(old_win, base.Internal):
old_win._grab_click()
self.qtile.core._root.set_property("_NET_ACTIVE_WINDOW", self.window.wid)
self._ungrab_click()
if self.group:
self.group.current_window = self
@ -1059,7 +1098,10 @@ class Internal(_Window, base.Internal):
return Drawer(self.qtile, self, width, height)
def kill(self):
self.qtile.core.conn.conn.core.DestroyWindow(self.window.wid)
if self.window.wid in self.qtile.windows_map:
# It will be present during config reloads; absent during shutdown as this
# will follow graceful_shutdown
self.qtile.core.conn.conn.core.DestroyWindow(self.window.wid)
def cmd_kill(self):
self.kill()
@ -1105,29 +1147,15 @@ class Static(_Window, base.Static):
self.conf_y = y
self.conf_width = width
self.conf_height = height
x = x or 0
y = y or 0
x = x or self.x
y = y or self.y
self.x = x + screen.x
self.y = y + screen.y
self.width = width or 0
self.height = height or 0
self.screen = screen
self.place(self.x, self.y, self.width, self.height, 0, 0)
self.place(self.x, self.y, width or self.width, height or self.height, 0, 0)
self.unhide()
self.update_strut()
# Grab button 1 to focus upon click
for amask in self.qtile.core._auto_modmasks():
self.qtile.core.conn.conn.core.GrabButton(
True,
self.window.wid,
EventMask.ButtonPress,
xcffib.xproto.GrabMode.Sync,
xcffib.xproto.GrabMode.Async,
xcffib.xproto.Atom._None,
xcffib.xproto.Atom._None,
1,
amask,
)
self._grab_click()
def handle_ConfigureRequest(self, e): # noqa: N802
cw = xcffib.xproto.ConfigWindow
@ -1208,38 +1236,12 @@ class Window(_Window, base.Window):
def __init__(self, window, qtile):
_Window.__init__(self, window, qtile)
self.update_name()
# 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 = self.is_transient_for()
if transient_for is not None:
group = transient_for._group
if group is not None:
group.add(self)
self._group = group
if group != qtile.current_screen.group:
self.hide()
self.set_group()
# add window to the save-set, so it gets mapped when qtile dies
qtile.core.conn.conn.core.ChangeSaveSet(SetMode.Insert, self.window.wid)
self.update_wm_net_icon()
# Grab button 1 to focus upon click
for amask in self.qtile.core._auto_modmasks():
self.qtile.core.conn.conn.core.GrabButton(
True,
self.window.wid,
EventMask.ButtonPress,
xcffib.xproto.GrabMode.Sync,
xcffib.xproto.GrabMode.Async,
xcffib.xproto.Atom._None,
xcffib.xproto.Atom._None,
1,
amask,
)
self._grab_click()
@property
def group(self):
@ -1313,11 +1315,12 @@ class Window(_Window, base.Window):
screen = self.group.screen or \
self.qtile.find_closest_screen(self.x, self.y)
bw = self.group.floating_layout.fullscreen_border_width
self._enablefloating(
screen.x,
screen.y,
screen.width,
screen.height,
screen.width - 2 * bw,
screen.height - 2 * bw,
new_float_state=FloatStates.FULLSCREEN
)
set_state(prev_state, prev_state | atom)
@ -1343,11 +1346,12 @@ class Window(_Window, base.Window):
screen = self.group.screen or \
self.qtile.find_closest_screen(self.x, self.y)
bw = self.group.floating_layout.max_border_width
self._enablefloating(
screen.dx,
screen.dy,
screen.dwidth,
screen.dheight,
screen.dwidth - 2 * bw,
screen.dheight - 2 * bw,
new_float_state=FloatStates.MAXIMIZED
)
else:
@ -1397,6 +1401,7 @@ class Window(_Window, base.Window):
self.group.remove(self)
s = Static(self.window, self.qtile, screen, x, y, width, height)
self.qtile.windows_map[self.window.wid] = s
self.qtile.core.update_client_list(self.qtile.windows_map)
hook.fire("client_managed", s)
def tweak_float(self, x=None, y=None, dx=0, dy=0,
@ -1466,6 +1471,22 @@ class Window(_Window, base.Window):
self.height = h
self._reconfigure_floating(new_float_state=new_float_state)
def set_group(self):
# add to group by position according to _NET_WM_DESKTOP property
group = None
index = self.window.get_wm_desktop()
if index is not None and index < len(self.qtile.groups):
group = self.qtile.groups[index]
elif index is None:
transient_for = self.is_transient_for()
if transient_for is not None:
group = transient_for._group
if group is not None:
group.add(self)
self._group = group
if group != self.qtile.current_screen.group:
self.hide()
def togroup(self, group_name=None, *, switch_group=False):
"""Move window to a specified group
@ -1492,17 +1513,6 @@ class Window(_Window, base.Window):
if switch_group:
group.cmd_toscreen(toggle=False)
def toscreen(self, index=None):
"""Move window to a specified screen, or the current screen."""
if index is None:
screen = self.qtile.current_screen
else:
try:
screen = self.qtile.screens[index]
except IndexError:
raise CommandError('No such screen: %d' % index)
self.togroup(screen.group.name)
def match(self, match):
"""Match window against given attributes.
@ -1618,33 +1628,33 @@ class Window(_Window, base.Window):
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")
logger.debug("Focusing window by pager")
self.qtile.current_screen.set_group(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":
logger.info("Focusing window")
logger.debug("Focusing window")
self.qtile.current_screen.set_group(self.group)
self.group.focus(self)
elif focus_behavior == "smart":
if not self.group.screen:
logger.info("Ignoring focus request")
logger.debug("Ignoring focus request")
return
if self.group.screen == self.qtile.current_screen:
logger.info("Focusing window")
logger.debug("Focusing window")
self.qtile.current_screen.set_group(self.group)
self.group.focus(self)
else: # self.group.screen != self.qtile.current_screen:
logger.info("Setting urgent flag for window")
logger.debug("Setting urgent flag for window")
self.urgent = True
elif focus_behavior == "urgent":
logger.info("Setting urgent flag for window")
logger.debug("Setting urgent flag for window")
self.urgent = True
elif focus_behavior == "never":
logger.info("Ignoring focus request")
logger.debug("Ignoring focus request")
else:
logger.warning("Invalid value for focus_on_window_activation: {}".format(focus_behavior))
logger.debug("Invalid value for focus_on_window_activation: {}".format(focus_behavior))
elif atoms["_NET_CLOSE_WINDOW"] == opcode:
self.kill()
elif atoms["WM_CHANGE_STATE"] == opcode:
@ -1654,11 +1664,10 @@ class Window(_Window, base.Window):
elif state == IconicState and self.qtile.config.auto_minimize:
self.minimized = True
else:
logger.info("Unhandled client message: %s", atoms.get_name(opcode))
logger.debug("Unhandled client message: %s", atoms.get_name(opcode))
def handle_PropertyNotify(self, e): # noqa: N802
name = self.qtile.core.conn.atoms.get_name(e.atom)
logger.debug("PropertyNotifyEvent: %s", name)
if name == "WM_TRANSIENT_FOR":
pass
elif name == "WM_HINTS":
@ -1694,16 +1703,19 @@ class Window(_Window, base.Window):
# self.update_state()
self.update_state()
else:
logger.info("Unknown window property: %s", name)
logger.debug("Unknown window property: %s", name)
return False
def _items(self, name: str) -> ItemT:
if name == "group":
return True, []
elif name == "layout":
return True, list(range(len(self.group.layouts)))
elif name == "screen" and self.group.screen is not None:
return True, []
if name == "layout":
if self.group:
return True, list(range(len(self.group.layouts)))
return None
if name == "screen":
if self.group and self.group.screen:
return True, []
return None
def _select(self, name, sel):
@ -1725,47 +1737,6 @@ class Window(_Window, base.Window):
"""
self.kill()
def cmd_togroup(self, groupName=None, *, switch_group=False): # noqa: 803
"""Move window to a specified group.
If groupName is not specified, we assume the current group.
If switch_group is True, also switch to that group.
Examples
========
Move window to current group::
togroup()
Move window to group "a"::
togroup("a")
Move window to group "a", and switch to group "a"::
togroup("a", switch_group=True)
"""
self.togroup(groupName, switch_group=switch_group)
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):
"""Move window by dx and dy"""
self.tweak_float(dx=dx, dy=dy)
@ -1823,9 +1794,6 @@ class Window(_Window, base.Window):
else:
self._reconfigure_floating() # atomatically above
def cmd_match(self, *args, **kwargs):
return self.match(*args, **kwargs)
def _is_in_window(self, x, y, window):
return (window.edges[0] <= x <= window.edges[2] and
window.edges[1] <= y <= window.edges[3])

View File

@ -170,7 +170,8 @@ PropertyMap = {
"_NET_WM_STRUT": ("CARDINAL", 32),
"_NET_WM_STRUT_PARTIAL": ("CARDINAL", 32),
"_NET_WM_WINDOW_OPACITY": ("CARDINAL", 32),
"_NET_WM_WINDOW_TYPE": ("CARDINAL", 32),
"_NET_WM_WINDOW_TYPE": ("ATOM", 32),
"_NET_FRAME_EXTENTS": ("CARDINAL", 32),
# Net State
"_NET_WM_STATE": ("ATOM", 32),
# Xembed

View File

@ -21,11 +21,12 @@
from __future__ import annotations
import typing
from collections import defaultdict
from libqtile import configurable
from libqtile.command.base import CommandObject, ItemT
from libqtile.log_utils import logger
from libqtile.utils import has_transparency
from libqtile.utils import has_transparency, rgb
if typing.TYPE_CHECKING:
from libqtile.widget.base import _Widget
@ -162,6 +163,8 @@ class Bar(Gap, configurable.Configurable):
("background", "#000000", "Background colour."),
("opacity", 1, "Bar window opacity."),
("margin", 0, "Space around bar as int or list of ints [N E S W]."),
("border_color", "#000000", "Border colour as str or list of str [N E S W]"),
("border_width", 0, "Width of border as int of list of ints [N E S W]")
]
def __init__(self, widgets, size, **config):
@ -175,45 +178,71 @@ class Bar(Gap, configurable.Configurable):
self.size_calculated = 0
self._configured = False
if isinstance(self.margin, int):
self.margin = [self.margin] * 4
if isinstance(self.border_width, int):
self.border_width = [self.border_width] * 4
self._initial_margin = self.margin[:]
self.struts = [0, 0, 0, 0]
self._add_strut = False
self.queued_draws = 0
self.future = None
self._borders_drawn = False
def _configure(self, qtile, screen):
Gap._configure(self, qtile, screen)
# We only want to adjust margin sizes once unless there's a new strut
if not self._configured or self._add_strut:
Gap._configure(self, qtile, screen)
self._borders_drawn = False
if self.margin:
if isinstance(self.margin, int):
self.margin = [self.margin] * 4
if self.horizontal:
self.x += self.margin[3]
self.width -= self.margin[1] + self.margin[3]
self.length = self.width
if self.size == self.initial_size:
self.size += self.margin[0] + self.margin[2]
if self.screen.top is self:
self.y += self.margin[0]
else:
self.y -= self.margin[2]
else:
self.y += self.margin[0]
self.height -= self.margin[0] + self.margin[2]
self.length = self.height
self.size += self.margin[1] + self.margin[3]
if self.screen.left is self:
self.x += self.margin[3]
else:
self.x -= self.margin[1]
if sum(self._initial_margin) or sum(self.border_width) or self._add_strut:
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)
try:
# Check if colours are valid but don't convert to rgba here
if isinstance(self.border_color, list) and len(self.border_color) == 4:
[rgb(col) for col in self.border_color]
else:
rgb(self.border_color)
self.border_color = [self.border_color] * 4
except (ValueError, TypeError):
logger.warning("Invalid border_color specified. Borders will not be displayed.")
self.border_width = [0, 0, 0, 0]
# Increase the margin size for the border. The border will be drawn
# in this space so the empty space will just be the margin.
self.margin = [m + b + s for m, b, s in zip(self._initial_margin, self.border_width, self.struts)]
if self.horizontal:
self.x += self.margin[3] - self.border_width[3]
self.width -= (self.margin[1] + self.margin[3])
self.length = self.width
if self.size == self.initial_size:
self.size += (self.margin[0] + self.margin[2])
if self.screen.top is self:
self.y += self.margin[0] - self.border_width[0]
else:
self.y -= self.margin[2] + self.border_width[2]
else:
self.y += self.margin[0] - self.border_width[0]
self.height -= (self.margin[0] + self.margin[2])
self.length = self.height
self.size += (self.margin[1] + self.margin[3])
if self.screen.left is self:
self.x += self.margin[3]
else:
self.x -= self.margin[1]
width = self.width + (self.border_width[1] + self.border_width[3])
height = self.height + (self.border_width[0] + self.border_width[2])
if self.window:
# We get _configure()-ed with an existing window when screens are getting
# reconfigured but this screen is present both before and after
self.window.place(self.x, self.y, self.width, self.height, 0, None)
self.window.place(self.x, self.y, width, height, 0, None)
else:
# Whereas we won't have a window if we're startup up for the first time or
# the window has been killed by us no longer using the bar's screen
@ -225,18 +254,16 @@ class Bar(Gap, configurable.Configurable):
depth = 32 if has_transparency(self.background) else self.qtile.core.conn.default_screen.root_depth
self.window = self.qtile.core.create_internal(
self.x, self.y, self.width, self.height, depth
self.x, self.y, width, height, depth
)
else:
self.window = self.qtile.core.create_internal(
self.x, self.y, self.width, self.height
)
self.window = self.qtile.core.create_internal(self.x, self.y, width, height)
self.window.opacity = self.opacity
self.window.unhide()
self.drawer = self.window.create_drawer(self.width, self.height)
self.drawer = self.window.create_drawer(width, height)
self.drawer.clear(self.background)
self.window.process_window_expose = self.draw
@ -264,11 +291,18 @@ class Bar(Gap, configurable.Configurable):
self.draw()
self._resize(self.length, self.widgets)
self._configured = True
self._add_strut = False
def _configure_widget(self, widget):
configured = True
try:
widget._configure(self.qtile, self)
if self.horizontal:
widget.offsety = self.border_width[0]
else:
widget.offsetx = self.border_width[3]
widget.configured = True
except Exception as e:
logger.error(
@ -288,11 +322,34 @@ class Bar(Gap, configurable.Configurable):
index = self.widgets.index(i)
crash = ConfigErrorWidget(widget=i)
crash._configure(self.qtile, self)
if self.horizontal:
crash.offsety = self.border_width[0]
else:
crash.offsetx = self.border_width[3]
self.widgets.insert(index, crash)
self.widgets.remove(i)
def _items(self, name: str) -> ItemT:
if name == "screen" and self.screen is not None:
return True, []
elif name == "widget" and self.widgets:
return False, [w.name for w in self.widgets]
return None
def _select(self, name, sel):
if name == "screen":
return self.screen
elif name == "widget":
for widget in self.widgets:
if widget.name == sel:
return widget
return None
def finalize(self):
self.future.cancel()
self.drawer.finalize()
self.window.kill()
self.widgets.clear()
def kill_window(self):
"""Kill the window when the bar's screen is no longer being used."""
@ -301,13 +358,27 @@ class Bar(Gap, configurable.Configurable):
self.window = None
def _resize(self, length, widgets):
stretches = [i for i in widgets if i.length_type == STRETCH]
# We want consecutive stretch widgets to split one 'block' of space between them
stretches = []
consecutive_stretches = defaultdict(list)
prev_stretch = None
for widget in widgets:
if widget.length_type == STRETCH:
if prev_stretch:
consecutive_stretches[prev_stretch].append(widget)
else:
stretches.append(widget)
prev_stretch = widget
else:
prev_stretch = None
if stretches:
stretchspace = length - sum(
[i.length for i in widgets if i.length_type != STRETCH]
)
stretchspace = max(stretchspace, 0)
num_stretches = len(stretches)
if num_stretches == 1:
stretches[0].length = stretchspace
else:
@ -316,12 +387,13 @@ class Bar(Gap, configurable.Configurable):
for i in widgets:
if i.length_type != STRETCH:
block += i.length
else:
elif i in stretches: # False for consecutive_stretches
blocks.append(block)
block = 0
if block:
blocks.append(block)
interval = length // num_stretches
for idx, i in enumerate(stretches):
if idx == 0:
i.length = interval - blocks[0] - blocks[1] // 2
@ -330,18 +402,27 @@ class Bar(Gap, configurable.Configurable):
else:
i.length = int(interval - blocks[idx] / 2 - blocks[idx + 1] / 2)
stretchspace -= i.length
stretches[0].length += stretchspace // 2
stretches[-1].length += stretchspace - stretchspace // 2
offset = 0
for i, followers in consecutive_stretches.items():
length = i.length // (len(followers) + 1)
rem = i.length - length
i.length = length
for f in followers:
f.length = length
rem -= length
i.length += rem
if self.horizontal:
offset = self.border_width[3]
for i in widgets:
i.offsetx = offset
i.offsety = 0
offset += i.length
else:
offset = self.border_width[0]
for i in widgets:
i.offsetx = 0
i.offsety = offset
offset += i.length
@ -430,12 +511,64 @@ class Bar(Gap, configurable.Configurable):
if not self.widgets:
return # calling self._actual_draw in this case would cause a NameError.
if self.queued_draws == 0:
self.qtile.call_soon(self._actual_draw)
self.future = 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)
# We draw the border before the widgets
if any(self.border_width) and not self._borders_drawn:
# The border is drawn "outside" of the bar (i.e. not in the space that the
# widgets occupy) so we need to add the additional space
width = self.width + self.border_width[1] + self.border_width[3]
height = self.height + self.border_width[0] + self.border_width[2]
# line_opts is a list of tuples where each tuple represents the borders
# in the order N, E, S, W. The border tuple contains two pairs of
# co-ordinates for the start and end of the border.
line_opts = [
(
(0, self.border_width[0] * 0.5),
(width, self.border_width[0] * 0.5)
),
(
(width - (self.border_width[1] * 0.5), self.border_width[0]),
(width - (self.border_width[1] * 0.5), height - self.border_width[2])
),
(
(0, height - self.border_width[2] + (self.border_width[2] * 0.5)),
(width, height - self.border_width[2] + (self.border_width[2] * 0.5))
),
(
(self.border_width[3] * 0.5, self.border_width[0]),
(self.border_width[3] * 0.5, height - self.border_width[2])
)
]
self.drawer.clear(self.background)
for border_width, colour, opts in zip(self.border_width, self.border_color, line_opts):
if not border_width:
continue
move_to, line_to = opts
# Draw the border
self.drawer.set_source_rgb(colour)
self.drawer.ctx.set_line_width(border_width)
self.drawer.ctx.move_to(*move_to)
self.drawer.ctx.line_to(*line_to)
self.drawer.ctx.stroke()
self.drawer.draw(0, 0)
# Prevent multiple redraws of borders
self._borders_drawn = True
for i in self.widgets:
i.draw()
end = i.offset + i.length # pylint: disable=undefined-loop-variable
@ -464,6 +597,7 @@ class Bar(Gap, configurable.Configurable):
if is_show != self.is_show():
if is_show:
self.size = self.size_calculated
self._borders_drawn = False
self.window.unhide()
else:
self.size_calculated = self.size
@ -474,16 +608,11 @@ class Bar(Gap, configurable.Configurable):
def adjust_for_strut(self, size):
if self.size:
self.size = self.initial_size
if not self.margin:
self.margin = [0, 0, 0, 0]
if self.screen.top is self:
self.margin[0] += size
elif self.screen.bottom is self:
self.margin[2] += size
elif self.screen.left is self:
self.margin[3] += size
else: # right
self.margin[1] += size
for i, gap in enumerate(["top", "right", "bottom", "left"]):
if getattr(self.screen, gap) is self:
self.struts[i] += size
self._add_strut = True
def cmd_fake_button_press(self, screen, position, x, y, button=1):
"""

View File

@ -128,12 +128,12 @@ class CommandObject(metaclass=abc.ABCMeta):
Return None if no such object exists
"""
def command(self, name: str) -> Callable:
def command(self, name: str) -> Optional[Callable]:
"""Return the command with the given name
Parameters
----------
name : str
name: str
The name of the command to fetch.
Returns
@ -170,6 +170,7 @@ class CommandObject(metaclass=abc.ABCMeta):
"""
if name in self.commands:
command = self.command(name)
assert command
signature = self._get_command_signature(command)
spec = name + signature
htext = inspect.getdoc(command) or ""

View File

@ -77,9 +77,9 @@ class CommandClient:
Parameters
----------
name : str
name: str
The name of the command graph object to resolve.
selector : Optional[str]
selector: Optional[str]
If given, the selector to use to select the next object, and if
None, then selects the default object.
@ -104,11 +104,11 @@ class CommandClient:
Parameters
----------
name : str
name: str
The name of the command to resolve in the command graph.
args :
args:
The arguments to pass into the call invocation.
kwargs :
kwargs:
The keyword arguments to pass into the call invocation.
Returns
@ -196,7 +196,7 @@ class InteractiveCommandClient:
Parameters
----------
name : str
name: str
The name of the element to resolve
Return
@ -206,6 +206,13 @@ class InteractiveCommandClient:
a command graph node (if the name is a valid child) or a command
graph call (if the name is a valid command).
"""
# Python's help() command will try to look up __name__ and __origin__ so we
# need to handle these explicitly otherwise they'll result in a SelectError
# which help() does not expect.
if name in ["__name__", "__origin__"]:
raise AttributeError
if isinstance(self._current_node, CommandGraphCall):
raise SelectError("Cannot select children of call", name, self._current_node.selectors)
@ -229,7 +236,7 @@ class InteractiveCommandClient:
Parameters
----------
name : str
name: str
The name, or index if it's of int type, of the item to resolve
Return
@ -270,6 +277,11 @@ def _normalize_item(object_type: Optional[str], item: str) -> Union[str, int]:
if object_type in ["group", "widget", "bar"]:
return str(item)
elif object_type in ["layout", "window", "screen"]:
return int(item)
try:
return int(item)
except ValueError:
# A value error could arise because the next selector has been passed
raise SelectError(f"Unexpected index {item}. Is this an object_type?",
str(object_type), [(str(object_type), str(item))])
else:
return item

View File

@ -129,7 +129,7 @@ class CommandGraphRoot(CommandGraphNode):
@property
def children(self) -> List[str]:
"""All of the child elements in the root of the command graph"""
return ["bar", "group", "layout", "screen", "widget", "window"]
return ["bar", "group", "layout", "screen", "widget", "window", "core"]
class CommandGraphObject(CommandGraphNode, metaclass=abc.ABCMeta):
@ -173,7 +173,7 @@ class CommandGraphObject(CommandGraphNode, metaclass=abc.ABCMeta):
class _BarGraphNode(CommandGraphObject):
object_type = "bar"
children = ["screen"]
children = ["screen", "widget"]
class _GroupGraphNode(CommandGraphObject):
@ -188,12 +188,12 @@ class _LayoutGraphNode(CommandGraphObject):
class _ScreenGraphNode(CommandGraphObject):
object_type = "screen"
children = ["layout", "window", "bar"]
children = ["layout", "window", "bar", "widget", "group"]
class _WidgetGraphNode(CommandGraphObject):
object_type = "widget"
children = ["bar", "screen", "group"]
children = ["bar", "screen"]
class _WindowGraphNode(CommandGraphObject):
@ -201,6 +201,11 @@ class _WindowGraphNode(CommandGraphObject):
children = ["group", "screen", "layout"]
class _CoreGraphNode(CommandGraphObject):
object_type = "core"
children: List[str] = []
_COMMAND_GRAPH_MAP: Dict[str, Type[CommandGraphObject]] = {
"bar": _BarGraphNode,
"group": _GroupGraphNode,
@ -208,4 +213,5 @@ _COMMAND_GRAPH_MAP: Dict[str, Type[CommandGraphObject]] = {
"widget": _WidgetGraphNode,
"window": _WindowGraphNode,
"screen": _ScreenGraphNode,
"core": _CoreGraphNode
}

View File

@ -87,9 +87,9 @@ class CommandInterface(metaclass=ABCMeta):
Parameters
----------
node : CommandGraphNode
node: CommandGraphNode
The node to check for commands
command : str
command: str
The name of the command to check for
Returns
@ -104,11 +104,11 @@ class CommandInterface(metaclass=ABCMeta):
Parameters
----------
node : CommandGraphNode
node: CommandGraphNode
The node to check for items
object_type : str
object_type: str
The type of object to check for items.
command : str
command: str
The name of the item to check for
Returns
@ -126,7 +126,7 @@ class QtileCommandInterface(CommandInterface):
Parameters
----------
command_object : CommandObject
command_object: CommandObject
The command object to use for resolving the commands and items
against.
"""
@ -148,10 +148,13 @@ class QtileCommandInterface(CommandInterface):
The keyword arguments to pass into the command graph call.
"""
obj = self._command_object.select(call.selectors)
cmd = None
try:
cmd = obj.command(call.name)
except SelectError:
pass
if cmd is None:
return "No such command."
logger.debug("Command: %s(%s, %s)", call.name, args, kwargs)
@ -162,9 +165,9 @@ class QtileCommandInterface(CommandInterface):
Parameters
----------
node : CommandGraphNode
node: CommandGraphNode
The node to check for commands
command : str
command: str
The name of the command to check for
Returns
@ -181,11 +184,11 @@ class QtileCommandInterface(CommandInterface):
Parameters
----------
node : CommandGraphNode
node: CommandGraphNode
The node to check for items
object_type : str
object_type: str
The type of object to check for items.
item : str
item: str
The name or index of the item to check for
Returns
@ -208,7 +211,7 @@ class IPCCommandInterface(CommandInterface):
Parameters
----------
ipc_client : ipc.Client
ipc_client: ipc.Client
The client that is to be used to resolve the calls.
"""
self._client = ipc_client
@ -245,9 +248,9 @@ class IPCCommandInterface(CommandInterface):
Parameters
----------
node : CommandGraphNode
node: CommandGraphNode
The node to check for commands
command : str
command: str
The name of the command to check for
Returns
@ -268,11 +271,11 @@ class IPCCommandInterface(CommandInterface):
Parameters
----------
node : CommandGraphNode
node: CommandGraphNode
The node to check for items
object_type : str
object_type: str
The type of object to check for items.
command : str
command: str
The name of the item to check for
Returns

View File

@ -37,7 +37,7 @@ from typing import TYPE_CHECKING, Callable, List, Optional, Union
from libqtile import configurable, hook, utils
from libqtile.backend import base
from libqtile.bar import BarType
from libqtile.bar import Bar, BarType
from libqtile.command.base import CommandObject, ItemT
if TYPE_CHECKING:
@ -389,6 +389,10 @@ class Screen(CommandObject):
return True, [i.wid for i in self.group.windows]
elif name == "bar":
return False, [x.position for x in self.gaps]
elif name == "widget":
return False, [w.name for g in self.gaps for w in g.widgets if isinstance(g, Bar)]
elif name == "group":
return True, [self.group.name]
return None
def _select(self, name, sel):
@ -406,6 +410,18 @@ class Screen(CommandObject):
return i
elif name == "bar":
return getattr(self, sel)
elif name == "widget":
for gap in self.gaps:
if not isinstance(gap, Bar):
continue
for widget in gap.widgets:
if widget.name == sel:
return widget
elif name == "group":
if sel is None:
return self.group
else:
return self.group if sel == self.group.name else None
def resize(self, x=None, y=None, w=None, h=None):
if x is None:
@ -526,20 +542,24 @@ class ScratchPad(Group):
Parameters
==========
name : string
name: string
the name of this group
dropdowns : default ``None``
dropdowns: default ``None``
list of DropDown objects
position : int
position: int
group position
label : string
label: string
The display name of the ScratchPad group. Defaults to the empty string
such that the group is hidden in ``GroupList`` widget.
single : Boolean
Only one of the window among the specified dropdowns will be
visible at a time.
"""
def __init__(self, name, dropdowns=None, position=sys.maxsize, label=''):
def __init__(self, name, dropdowns=None, position=sys.maxsize, label='', single=False):
Group.__init__(self, name, layout='floating', layouts=['floating'],
init=False, position=position, label=label)
self.dropdowns = dropdowns if dropdowns is not None else []
self.single = single
def __repr__(self):
return '<config.ScratchPad %r (%s)>' % (
@ -577,7 +597,7 @@ class Match:
"""
def __init__(self, title=None, wm_class=None, role=None, wm_type=None,
wm_instance_class=None, net_wm_pid=None,
func: Callable[[base.WindowType], bool] = None):
func: Callable[[base.WindowType], bool] = None, wid=None):
self._rules = {}
if title is not None:
@ -586,6 +606,8 @@ class Match:
self._rules["wm_class"] = wm_class
if wm_instance_class is not None:
self._rules["wm_instance_class"] = wm_instance_class
if wid is not None:
self._rules["wid"] = wid
if net_wm_pid is not None:
try:
self._rules["net_wm_pid"] = int(net_wm_pid)
@ -603,7 +625,7 @@ class Match:
@staticmethod
def _get_property_predicate(name, value):
if name == 'net_wm_pid':
if name == 'net_wm_pid' or name == 'wid':
return lambda other: other == value
elif name == 'wm_class':
def predicate(other):
@ -636,6 +658,8 @@ class Match:
return rule_value(client)
elif property_name == 'net_wm_pid':
value = client.get_pid()
elif property_name == "wid":
value = client.window.wid
else:
value = client.get_wm_type()
@ -745,6 +769,14 @@ class DropDown(configurable.Configurable):
'This has only effect if any of the on_focus_lost_xxx '
'configurations is True'
),
(
'match',
None,
"Use a ``config.Match`` to identify the spawned window and move it to the "
"scratchpad, instead of relying on the window's PID. This works around "
"some programs that may not be caught by the window's PID if it does "
"not match the PID of the spawned process."
),
)
def __init__(self, name, cmd, **config):
@ -755,10 +787,12 @@ class DropDown(configurable.Configurable):
Parameters
==========
name : string
name: string
The name of the DropDown configuration.
cmd : string
cmd: string
Command to spawn a process.
match : Match
A match object to identify the window instead of the pid.
"""
configurable.Configurable.__init__(self, **config)
self.name = name

View File

@ -31,7 +31,7 @@ from typing import TYPE_CHECKING
from libqtile.backend.x11 import core
if TYPE_CHECKING:
from typing import Any, Dict, List
from typing import Any, Dict, List, Union
from typing_extensions import Literal
@ -44,7 +44,7 @@ class ConfigError(Exception):
config_pyi_header = """
from typing import Any, Dict, List
from typing import Any, Dict, List, Union
from typing_extensions import Literal
from libqtile.config import Group, Key, Mouse, Rule, Screen
from libqtile.layout.base import Layout
@ -68,7 +68,7 @@ class Config:
auto_fullscreen: bool
widget_defaults: Dict[str, Any]
extension_defaults: Dict[str, Any]
bring_front_click: bool
bring_front_click: Union[bool, Literal["floating_only"]]
reconfigure_screens: bool
wmname: str
auto_minimize: bool

View File

@ -1,10 +1,15 @@
from __future__ import annotations
import asyncio
import contextlib
import signal
from typing import Callable, Dict, Optional
from typing import TYPE_CHECKING, Callable, Dict, Optional
from libqtile.log_utils import logger
if TYPE_CHECKING:
from libqtile.core.manager import Qtile
class LoopContext(contextlib.AbstractAsyncContextManager):
def __init__(
@ -59,3 +64,20 @@ class LoopContext(contextlib.AbstractAsyncContextManager):
logger.exception(exc)
else:
logger.error(f'unhandled error in event loop: {context["msg"]}')
class QtileEventLoopPolicy(asyncio.DefaultEventLoopPolicy): # type: ignore
"""
Asyncio policy to ensure the main event loop is accessible
even if `get_event_loop()` is called from a different thread.
"""
def __init__(self, qtile: Qtile) -> None:
asyncio.DefaultEventLoopPolicy.__init__(self)
self.qtile = qtile
def get_event_loop(self) -> asyncio.AbstractEventLoop:
if isinstance(self.qtile._eventloop, asyncio.AbstractEventLoop):
return self.qtile._eventloop
raise RuntimeError

View File

@ -33,7 +33,7 @@ import tempfile
from typing import TYPE_CHECKING
import libqtile
from libqtile import bar, confreader, hook, ipc, utils
from libqtile import bar, hook, ipc, utils
from libqtile.backend import base
from libqtile.command import interface
from libqtile.command.base import (
@ -48,7 +48,7 @@ from libqtile.config import Click, Drag, Key, KeyChord, Match, Rule
from libqtile.config import ScratchPad as ScratchPadConfig
from libqtile.config import Screen
from libqtile.core.lifecycle import lifecycle
from libqtile.core.loop import LoopContext
from libqtile.core.loop import LoopContext, QtileEventLoopPolicy
from libqtile.core.state import QtileState
from libqtile.dgroups import DGroups
from libqtile.extension.base import _Extension
@ -69,7 +69,6 @@ if TYPE_CHECKING:
class Qtile(CommandObject):
"""This object is the `root` of the command graph"""
# These are assigned values in _configure
current_screen: Screen
dgroups: DGroups
_eventloop: asyncio.AbstractEventLoop
@ -85,7 +84,7 @@ class Qtile(CommandObject):
self.core = kore
self.config = config
self.no_spawn = no_spawn
self._state = state
self._state: Optional[Union[QtileState, str]] = state
self.socket_path = socket_path
self._drag: Optional[Tuple] = None
@ -106,28 +105,21 @@ class Qtile(CommandObject):
self._stopped_event: Optional[asyncio.Event] = None
self.server = IPCCommandServer(self)
self.load_config()
def load_config(self) -> None:
def load_config(self, initial=False) -> None:
try:
self.config.load()
self.config.validate()
except Exception as e:
logger.exception('Error while reading config file (%s)', e)
self.config = confreader.Config()
from libqtile.widget import TextBox
widgets = self.config.screens[0].bottom.widgets # type: ignore
widgets.insert(0, TextBox('Config Err!'))
send_notification("Configuration error", str(e))
if hasattr(self.core, "wmname"):
self.core.wmname = getattr(self.config, "wmname", "qtile") # type: ignore
self.dgroups = DGroups(self, self.config.groups, self.config.dgroups_key_binder)
if self.config.widget_defaults:
_Widget.global_defaults = self.config.widget_defaults
if self.config.extension_defaults:
_Extension.global_defaults = self.config.extension_defaults
_Widget.global_defaults = self.config.widget_defaults
_Extension.global_defaults = self.config.extension_defaults
for installed_extension in _Extension.installed_extensions:
installed_extension._configure(self)
@ -137,26 +129,13 @@ class Qtile(CommandObject):
for grp in self.config.groups:
if isinstance(grp, ScratchPadConfig):
sp = ScratchPad(grp.name, grp.dropdowns, grp.label)
sp = ScratchPad(grp.name, grp.dropdowns, grp.label, grp.single)
sp._configure([self.config.floating_layout],
self.config.floating_layout, self)
self.groups.append(sp)
self.groups_map[sp.name] = sp
def dump_state(self, buf) -> None:
try:
pickle.dump(QtileState(self), buf, protocol=0)
except: # noqa: E722
logger.exception('Unable to pickle qtile state')
def _configure(self) -> None:
"""
This is the part of init that needs to happen after the event loop is
fully set up. asyncio is required to listen and respond to backend
events.
"""
self._process_screens()
self.current_screen = self.screens[0]
self._process_screens(reloading=not initial)
# Map and Grab keys
for key in self.config.keys:
@ -165,23 +144,28 @@ class Qtile(CommandObject):
for button in self.config.mouse:
self.grab_button(button)
# no_spawn is set when we are restarting; we only want to run the
# no_spawn is set after the very first startup; we only want to run the
# startup hook once.
if not self.no_spawn:
hook.fire("startup_once")
self.no_spawn = True
hook.fire("startup")
if self._state:
try:
with open(self._state, 'rb') as f:
st = pickle.load(f)
st.apply(self)
except: # noqa: E722
logger.exception("failed restoring state")
finally:
os.remove(self._state)
if isinstance(self._state, str):
try:
with open(self._state, 'rb') as f:
st = pickle.load(f)
st.apply(self)
except: # noqa: E722
logger.exception("failed restoring state")
finally:
os.remove(self._state)
else:
self._state.apply(self)
self.core.distribute_windows(initial)
self.core.scan()
if self._state:
for screen in self.screens:
screen.group.layout_all()
@ -192,7 +176,8 @@ class Qtile(CommandObject):
if self.config.reconfigure_screens:
hook.subscribe.screen_change(self.cmd_reconfigure_screens)
hook.fire("startup_complete")
if initial:
hook.fire("startup_complete")
def _prepare_socket_path(
self,
@ -215,6 +200,8 @@ class Qtile(CommandObject):
Finalizes the Qtile instance on exit.
"""
self._eventloop = asyncio.get_running_loop()
# Set the event loop policy to facilitate access to main event loop
asyncio.set_event_loop_policy(QtileEventLoopPolicy(self))
self._stopped_event = asyncio.Event()
self.core.setup_listener(self)
try:
@ -222,11 +209,13 @@ class Qtile(CommandObject):
signal.SIGTERM: self.stop,
signal.SIGINT: self.stop,
signal.SIGHUP: self.stop,
signal.SIGUSR1: self.cmd_reload_config,
signal.SIGUSR2: self.cmd_restart,
}), ipc.Server(
self._prepare_socket_path(self.socket_path),
self.server.call,
):
self._configure()
self.load_config(initial=True)
await self._stopped_event.wait()
finally:
self.finalize()
@ -252,10 +241,48 @@ class Qtile(CommandObject):
if self._stopped_event is not None:
self._stopped_event.set()
def finalize(self) -> None:
def dump_state(self, buf) -> None:
try:
pickle.dump(QtileState(self), buf, protocol=0)
except: # noqa: E722
logger.exception('Unable to pickle qtile state')
def cmd_reload_config(self) -> None:
"""
Reload the configuration file.
Can also be triggered by sending Qtile a SIGUSR1 signal.
"""
logger.debug('Reloading the configuration file')
try:
self.config.load()
except Exception as error:
logger.error("Configuration error: {}".format(error))
send_notification("Configuration error", str(error))
return
self._state = QtileState(self, restart=False)
self._finalize_configurables()
hook.clear()
self.ungrab_keys()
self.chord_stack.clear()
self.core.ungrab_buttons()
self.mouse_map.clear()
self.groups_map.clear()
self.groups.clear()
self.screens.clear()
self.load_config()
def _finalize_configurables(self) -> None:
"""
Finalize objects that are instantiated within the config file. In addition to
shutdown, these are finalized and then regenerated when reloading the config.
"""
try:
for widget in self.widgets_map.values():
widget.finalize()
self.widgets_map.clear()
for layout in self.config.layouts:
layout.finalize()
@ -265,12 +292,14 @@ class Qtile(CommandObject):
gap.finalize()
except: # noqa: E722
logger.exception('exception during finalize')
finally:
hook.clear()
self.core.finalize()
hook.clear()
def _process_screens(self) -> None:
current_groups = [screen.group for screen in self.screens if screen.group]
def finalize(self) -> None:
self._finalize_configurables()
self.core.finalize()
def _process_screens(self, reloading=False) -> None:
current_groups = [s.group for s in self.screens if hasattr(s, "group")]
screens = []
if hasattr(self.config, 'fake_screens'):
@ -293,8 +322,9 @@ class Qtile(CommandObject):
else:
scr = config[i]
if not hasattr(self, "current_screen"):
if not hasattr(self, "current_screen") or reloading:
self.current_screen = scr
reloading = False
if len(self.groups) < i + 1:
name = f"autogen_{i + 1}"
@ -344,7 +374,7 @@ class Qtile(CommandObject):
def process_key_event(self, keysym: int, mask: int) -> None:
key = self.keys_map.get((keysym, mask), None)
if key is None:
logger.info("Ignoring unknown keysym: {keysym}, mask: {mask}".format(keysym=keysym, mask=mask))
logger.debug("Ignoring unknown keysym: {keysym}, mask: {mask}".format(keysym=keysym, mask=mask))
return
if isinstance(key, KeyChord):
@ -661,7 +691,8 @@ class Qtile(CommandObject):
def process_button_click(
self, button_code: int, modmask: int, x: int, y: int
) -> None:
) -> bool:
handled = False
for m in self.mouse_map.get(button_code, []):
if not m.modmask == modmask:
continue
@ -675,6 +706,7 @@ class Qtile(CommandObject):
logger.error(
"Mouse command error %s: %s" % (i.name, val)
)
handled = True
elif isinstance(m, Drag):
if m.start:
i = m.start
@ -689,13 +721,18 @@ class Qtile(CommandObject):
val = (0, 0)
self._drag = (x, y, val[0], val[1], m.commands)
self.core.grab_pointer()
handled = True
def process_button_release(self, button_code: int, modmask: int) -> None:
for m in self.mouse_map.get(button_code, []):
if isinstance(m, Drag):
self._drag = None
self.core.ungrab_pointer()
return
return handled
def process_button_release(self, button_code: int, modmask: int) -> bool:
if self._drag is not None:
for m in self.mouse_map.get(button_code, []):
if isinstance(m, Drag):
self._drag = None
self.core.ungrab_pointer()
return True
return False
def process_button_motion(self, x: int, y: int) -> None:
if self._drag is None:
@ -756,6 +793,8 @@ class Qtile(CommandObject):
return True, list(self.windows_map.keys())
elif name == "screen":
return True, list(range(len(self.screens)))
elif name == "core":
return True, []
return None
def _select(self, name: str, sel: Optional[Union[str, int]]) -> Optional[CommandObject]:
@ -783,6 +822,8 @@ class Qtile(CommandObject):
return self.current_screen
else:
return utils.lget(self.screens, sel)
elif name == "core":
return self.core
return None
def call_soon(self, func: Callable, *args) -> asyncio.Handle:
@ -1018,17 +1059,24 @@ class Qtile(CommandObject):
try:
self.config.load()
except Exception as error:
send_notification("Configuration check", str(error.__context__))
send_notification("Configuration check", str(error))
else:
send_notification("Configuration check", "No error found!")
def cmd_restart(self) -> None:
"""Restart qtile"""
"""
Restart Qtile.
Can also be triggered by sending Qtile a SIGUSR2 signal.
"""
if not self.core.supports_restarting:
raise CommandError(f"Backend does not support restarting: {self.core.name}")
try:
self.config.load()
except Exception as error:
logger.error("Preventing restart because of a configuration error: {}".format(error))
send_notification("Configuration error", str(error.__context__))
send_notification("Configuration error", str(error))
return
self.restart()
@ -1282,11 +1330,11 @@ class Qtile(CommandObject):
try:
self.groups_map[group].cmd_toscreen()
except KeyError:
logger.info("No group named '{0:s}' present.".format(group))
logger.warning("No group named '{0:s}' present.".format(group))
mb = self.widgets_map.get(widget)
if not mb:
logger.warning("No widget named '{0:s}' present.".format(widget))
logger.error("No widget named '{0:s}' present.".format(widget))
return
mb.start_input(prompt, f, "group", strict_completer=True)
@ -1317,6 +1365,7 @@ class Qtile(CommandObject):
command: str = "%s",
complete: str = "cmd",
shell: bool = True,
aliases: Optional[Dict[str, str]] = None,
) -> None:
"""Spawn a command using a prompt widget, with tab-completion.
@ -1330,9 +1379,16 @@ class Qtile(CommandObject):
command template (default: "%s").
complete :
Tab completion function (default: "cmd")
shell :
Execute the command with /bin/sh (default: True)
aliases :
Dictionary mapping aliases to commands. If the entered command is a key in
this dict, the command it maps to will be executed instead.
"""
def f(args):
if args:
if aliases and args in aliases:
args = aliases[args]
self.cmd_spawn(command % args, shell=shell)
try:
mb = self.widgets_map[widget]
@ -1371,7 +1427,7 @@ class Qtile(CommandObject):
return
cmd_len = len(cmd_arg)
if cmd_len == 0:
logger.info('No command entered.')
logger.debug('No command entered.')
return
try:
result = eval(u'c.{0:s}'.format(cmd))
@ -1466,7 +1522,7 @@ class Qtile(CommandObject):
else:
logger.warning("Not found bar for hide/show.")
else:
logger.error("Invalid position value:{0:s}".format(position))
logger.warning("Invalid position value:{0:s}".format(position))
def cmd_get_state(self) -> str:
"""Get pickled state for restarting qtile"""
@ -1510,7 +1566,3 @@ class Qtile(CommandObject):
def cmd_run_extension(self, extension: _Extension) -> None:
"""Run extensions"""
extension.run()
def cmd_change_vt(self, vt: int) -> bool:
"""Change virtual terminal, returning success."""
return self.core.change_vt(vt)

View File

@ -24,27 +24,26 @@ from libqtile.scratchpad import ScratchPad
class QtileState:
"""Represents the state of the qtile 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.
This is used for restoring state across restarts or config reloads.
If `restart` is True, the current set of groups will be saved in the state. This is
useful when restarting for Qtile version updates rather than reloading the config.
ScratchPad groups are saved for both reloading and restarting.
"""
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.
def __init__(self, qtile, restart=True):
self.groups = []
self.screens = {}
self.current_screen = 0
self.scratchpads = {}
self.orphans = []
self.restart = restart # True when restarting, False when config reloading
for group in qtile.groups:
if isinstance(group, ScratchPad):
self.scratchpads[group.name] = group.get_state()
for dd in group.dropdowns.values():
dd.hide()
else:
elif restart:
self.groups.append((group.name, group.layout.name, group.label))
for index, screen in enumerate(qtile.screens):
@ -73,13 +72,17 @@ class QtileState:
for group in qtile.groups:
if isinstance(group, ScratchPad) and group.name in self.scratchpads:
orphans = group.restore_state(self.scratchpads.pop(group.name))
orphans = group.restore_state(self.scratchpads.pop(group.name), self.restart)
self.orphans.extend(orphans)
for sp_state in self.scratchpads.values():
for _, pid, _ in sp_state:
self.orphans.append(pid)
for _, wid, _ in sp_state:
self.orphans.append(wid)
if self.orphans:
hook.subscribe.client_new(self.handle_orphan_dropdowns)
if self.restart:
hook.subscribe.client_new(self.handle_orphan_dropdowns)
else:
for wid in self.orphans:
qtile.windows_map[wid].group = qtile.current_group
qtile.focus_screen(self.current_screen)
@ -87,9 +90,9 @@ class QtileState:
"""
Remove any windows from now non-existent scratchpad groups.
"""
client_pid = client.get_pid()
if client_pid in self.orphans:
self.orphans.remove(client_pid)
client_wid = client.wid
if client_wid in self.orphans:
self.orphans.remove(client_wid)
client.group = None
if not self.orphans:
hook.unsubscribe.client_new(self.handle_orphan_dropdowns)

View File

@ -150,7 +150,7 @@ class DGroups:
def _add(self, client):
if client in self.timeout:
logger.info('Remove dgroup source')
logger.debug('Remove dgroup source')
self.timeout.pop(client).cancel()
# ignore static windows
@ -186,7 +186,7 @@ class DGroups:
group = self.groups_map.get(rule.group)
if group and group_added:
for k, v in list(group.layout_opts.items()):
if isinstance(v, collections.Callable):
if isinstance(v, collections.abc.Callable):
v(group_obj.layout)
else:
setattr(group_obj.layout, k, v)
@ -195,7 +195,7 @@ class DGroups:
self.qtile.screens[affinity].set_group(group_obj)
if rule.float:
client.enablefloating()
client.cmd_enable_floating()
if rule.intrusive:
intrusive = rule.intrusive
@ -249,8 +249,7 @@ class DGroups:
self.sort_groups()
del self.timeout[client]
# Wait the delay until really delete the group
logger.info('Add dgroup timer with delay {}s'.format(self.delay))
logger.debug(f'Deleting {group} in {self.delay}s')
self.timeout[client] = self.qtile.call_later(
self.delay, delete_client
)

View File

@ -141,13 +141,13 @@ class TextFrame:
self.drawer = self.layout.drawer
self.highlight_color = highlight_color
if isinstance(pad_x, collections.Iterable):
if isinstance(pad_x, collections.abc.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):
if isinstance(pad_y, collections.abc.Iterable):
self.pad_top = pad_y[0]
self.pad_bottom = pad_y[1]
else:

View File

@ -1,4 +1,5 @@
# Copyright (c) 2017 Dario Giovannetti
# Copyright (c) 2021 elParaguayo
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@ -17,12 +18,15 @@
# 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
import shlex
from subprocess import PIPE, Popen
from typing import Any, List, Tuple # noqa: F401
from libqtile import configurable
from libqtile.log_utils import logger
RGB = re.compile(r"^#?([a-fA-F0-9]{3}|[a-fA-F0-9]{6})$")
class _Extension(configurable.Configurable):
@ -33,10 +37,10 @@ class _Extension(configurable.Configurable):
defaults = [
("font", "sans", "defines the font name to be used"),
("fontsize", None, "defines the font size to be 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"),
("background", None, "defines the normal background color (#RGB or #RRGGBB)"),
("foreground", None, "defines the normal foreground color (#RGB or #RRGGBB)"),
("selected_background", None, "defines the selected background color (#RGB or #RRGGBB)"),
("selected_foreground", None, "defines the selected foreground color (#RGB or #RRGGBB)"),
]
def __init__(self, **config):
@ -44,8 +48,35 @@ class _Extension(configurable.Configurable):
self.add_defaults(_Extension.defaults)
_Extension.installed_extensions.append(self)
def _check_colors(self):
"""
dmenu needs colours to be in #rgb or #rrggbb format.
Checks colour value, removes invalid values and adds # if missing.
NB This should not be called in _Extension.__init__ as _Extension.global_defaults
may not have been set at this point.
"""
for c in ["background", "foreground", "selected_background", "selected_foreground"]:
col = getattr(self, c, None)
if col is None:
continue
if not isinstance(col, str) or not RGB.match(col):
logger.warning(
f"Invalid extension '{c}' color: {col}. "
f"Must be #RGB or #RRGGBB string."
)
setattr(self, c, None)
continue
if not col.startswith("#"):
col = f"#{col}"
setattr(self, c, col)
def _configure(self, qtile):
self.qtile = qtile
self._check_colors()
def run(self):
"""
@ -75,6 +106,7 @@ class RunCommand(_Extension):
def __init__(self, **config):
_Extension.__init__(self, **config)
self.add_defaults(RunCommand.defaults)
self.configured_command = None
def run(self):
"""

View File

@ -18,8 +18,6 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from os import system
from libqtile.extension.dmenu import Dmenu
@ -61,7 +59,7 @@ class CommandSet(Dmenu):
if self.pre_commands:
for cmd in self.pre_commands:
system(cmd)
self.qtile.cmd_spawn(cmd)
out = super(CommandSet, self).run(items=self.commands.keys())
@ -76,4 +74,4 @@ class CommandSet(Dmenu):
if sout not in self.commands:
return
system(self.commands[sout])
self.qtile.cmd_spawn(self.commands[sout])

View File

@ -44,7 +44,7 @@ class _Group(CommandObject):
self.name = name
self.label = name if label is None else label
self.custom_layout = layout # will be set on _configure
self.windows = set()
self.windows = []
self.qtile = None
self.layouts = []
self.floating_layout = None
@ -61,7 +61,7 @@ class _Group(CommandObject):
self.screen = None
self.current_layout = 0
self.focus_history = []
self.windows = set()
self.windows = []
self.qtile = qtile
self.layouts = [i.clone(self) for i in layouts]
self.floating_layout = floating_layout
@ -235,7 +235,8 @@ class _Group(CommandObject):
def add(self, win, focus=True, force=False):
hook.fire("group_window_add", self, win)
self.windows.add(win)
if win not in self.windows:
self.windows.append(win)
win.group = self
if self.qtile.config.auto_fullscreen and win.wants_to_fullscreen:
win._float_state = FloatStates.FULLSCREEN
@ -335,7 +336,7 @@ class _Group(CommandObject):
"""Returns a dictionary of info for this group"""
return self.info()
def cmd_toscreen(self, screen=None, toggle=True):
def cmd_toscreen(self, screen=None, toggle=False):
"""Pull a group to a specified screen.
Parameters

View File

@ -304,6 +304,19 @@ class Subscribe:
"""
return self._subscribe("screen_change", func)
def screens_reconfigured(self, func):
"""Called when all ``screen_change`` hooks have fired.
This is primarily useful where you want a callback to be triggered once
``qtile.cmd_reconfigure_screens`` has completed (e.g. if
``reconfigure_screens`` is set to ``True`` in your config).
**Arguments**
None
"""
return self._subscribe("screens_reconfigured", func)
def current_screen_change(self, func):
"""Called when the current screen (i.e. the screen with focus) changes

View File

@ -94,9 +94,9 @@ class _IPC:
Parameters
----------
data : bytes
data: bytes
The incoming message to unpack
is_json : Optional[bool]
is_json: Optional[bool]
If the message should be unpacked as json. By default, try to
unpack json and fallback gracefully to marshalled bytes.
@ -142,10 +142,10 @@ class Client:
Parameters
----------
socket_path : str
socket_path: str
The file path to the file that is used to open the connection to
the running IPC server.
is_json : bool
is_json: bool
Pack and unpack messages as json
"""
self.socket_path = socket_path

View File

@ -294,7 +294,7 @@ class _ClientList:
Positive values are after the client.
Use parameter 'client_position' to insert the given client at 4 specific
positions : top, bottom, after_current, before_current.
positions: top, bottom, after_current, before_current.
"""
if client_position is not None:
if client_position == "after_current":
@ -490,12 +490,15 @@ class _SimpleLayoutBase(Layout):
client = self.focus_next(self.clients.current_client) or self.focus_first()
self.group.focus(client, True)
def add(self, client, offset_to_current=0):
return self.clients.add(client, offset_to_current)
def add(self, client, offset_to_current=0, client_position=None):
return self.clients.add(client, offset_to_current, client_position)
def remove(self, client):
return self.clients.remove(client)
def get_windows(self):
return self.clients.clients
def info(self):
d = Layout.info(self)
d.update(self.clients.info())

View File

@ -171,6 +171,9 @@ class Bsp(Layout):
c.current = c.root
return c
def get_windows(self):
return list(self.root.clients())
def info(self):
return dict(
name=self.name,

View File

@ -147,6 +147,12 @@ class Columns(Layout):
c.columns = [_Column(self.split, self.insert_position)]
return c
def get_windows(self):
clients = []
for c in self.columns:
clients.extend(c.clients)
return clients
def info(self):
d = Layout.info(self)
d["clients"] = []

View File

@ -30,9 +30,9 @@
from typing import List, Optional
from libqtile.backend.base import Window
from libqtile.config import Match
from libqtile.layout.base import Layout
from libqtile.backend.base import Window
class Floating(Layout):
@ -289,6 +289,9 @@ class Floating(Layout):
self.clients.remove(client)
return next_focus
def get_windows(self):
return self.clients
def info(self):
d = Layout.info(self)
d["clients"] = [c.name for c in self.clients]

View File

@ -289,16 +289,11 @@ class RatioTile(_SimpleLayoutBase):
def info(self):
d = _SimpleLayoutBase.info(self)
focused = self.clients.current_client
d['ratio'] = self.ratio,
d['focused'] = focused.name if focused else None,
d['ratio'] = self.ratio
d['focused'] = focused.name if focused else None
d['layout_info'] = self.layout_info
return d
def shuffle(self, function):
if self.clients:
function(self.clients)
self.group.layout_all()
cmd_down = _SimpleLayoutBase.previous
cmd_up = _SimpleLayoutBase.next

View File

@ -95,6 +95,9 @@ class Single(Layout):
def cmd_previous(self):
pass
def get_windows(self):
return self.window
class Slice(Layout):
"""Slice layout
@ -265,6 +268,13 @@ class Slice(Layout):
def commands(self):
return self._get_active_layout().commands
def get_windows(self):
clients = list()
for layout in self._get_layouts():
if layout.get_windows() is not None:
clients.extend(layout.get_windows())
return clients
def info(self):
d = Layout.info(self)
for layout in self._get_layouts():

View File

@ -254,6 +254,9 @@ class Stack(Layout):
else:
client.hide()
def get_windows(self):
return self.clients
def info(self):
d = Layout.info(self)
d["stacks"] = [i.info() for i in self.stacks]

View File

@ -47,10 +47,14 @@ class Tile(_SimpleLayoutBase):
defaults = [
("border_focus", "#0000ff", "Border colour(s) for the focused window."),
("border_normal", "#000000", "Border colour(s) for un-focused windows."),
("border_on_single", True, "Whether to draw border if there is only one window."),
("border_width", 1, "Border width."),
("margin", 0, "Margin of the layout (int or list of ints [N E S W])"),
("margin_on_single", True, "Whether to draw margin if there is only one window."),
("ratio", 0.618,
"Width-percentage of screen size reserved for master windows."),
("max_ratio", 0.85, "Maximum width of master windows"),
("min_ratio", 0.15, "Minimum width of master windows"),
("master_length", 1,
"Amount of windows displayed in the master stack. Surplus windows "
"will be moved to the slave stack."),
@ -76,6 +80,15 @@ class Tile(_SimpleLayoutBase):
def __init__(self, **config):
_SimpleLayoutBase.__init__(self, **config)
self.add_defaults(Tile.defaults)
self._initial_ratio = self.ratio
@property
def ratio_size(self):
return self.ratio
@ratio_size.setter
def ratio_size(self, ratio):
self.ratio = min(max(ratio, self.min_ratio), self.max_ratio)
@property
def master_windows(self):
@ -129,21 +142,25 @@ class Tile(_SimpleLayoutBase):
if self.clients and client in self.clients:
pos = self.clients.index(client)
if client in self.master_windows:
w = int(screen_width * self.ratio) \
w = int(screen_width * self.ratio_size) \
if len(self.slave_windows) or not self.expand \
else screen_width
h = screen_height // self.master_length
x = screen_rect.x
y = screen_rect.y + pos * h
else:
w = screen_width - int(screen_width * self.ratio)
w = screen_width - int(screen_width * self.ratio_size)
h = screen_height // (len(self.slave_windows))
x = screen_rect.x + int(screen_width * self.ratio)
x = screen_rect.x + int(screen_width * self.ratio_size)
y = screen_rect.y + self.clients[self.master_length:].index(client) * h
if client.has_focus:
bc = self.border_focus
else:
bc = self.border_normal
if not self.border_on_single and len(self.clients) == 1:
border_width = 0
else:
border_width = self.border_width
client.place(
x,
y,
@ -151,7 +168,7 @@ class Tile(_SimpleLayoutBase):
h - border_width * 2,
border_width,
bc,
margin=self.margin,
margin=0 if (not self.margin_on_single and len(self.clients) == 1) else self.margin,
)
client.unhide()
else:
@ -171,6 +188,12 @@ class Tile(_SimpleLayoutBase):
def cmd_shuffle_up(self):
self.up()
def cmd_reset(self):
self.ratio_size = self._initial_ratio
self.group.layout_all()
cmd_normalize = cmd_reset
cmd_shuffle_left = cmd_shuffle_up
cmd_shuffle_right = cmd_shuffle_down
@ -182,11 +205,11 @@ class Tile(_SimpleLayoutBase):
cmd_right = cmd_next
def cmd_decrease_ratio(self):
self.ratio -= self.ratio_increment
self.ratio_size -= self.ratio_increment
self.group.layout_all()
def cmd_increase_ratio(self):
self.ratio += self.ratio_increment
self.ratio_size += self.ratio_increment
self.group.layout_all()
def cmd_decrease_nmaster(self):

View File

@ -205,7 +205,7 @@ class Root(TreeNode):
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)
idx = max(self.children.index(sec), 1)
next_sec = self.children[idx - 1]
# delete old section, reparent children to next section
del self.children[idx]
@ -213,6 +213,8 @@ class Root(TreeNode):
for i in sec.children:
i.parent = next_sec
del self.sections[name]
class Section(TreeNode):
def __init__(self, title):
@ -385,6 +387,7 @@ class TreeTab(Layout):
("panel_width", 150, "Width of the left panel"),
("sections", ['Default'], "Foreground color of inactive tab"),
("previous_on_rm", False, "Focus previous window on close instead of first."),
("place_right", False, "Place the tab panel on the right side"),
]
def __init__(self, **config):
@ -498,6 +501,13 @@ class TreeTab(Layout):
if self._drawer is not None:
self._drawer.finalize()
def get_windows(self):
clients = []
for section in self._tree.children:
for window in section.children:
clients.append(window.window)
return clients
def info(self):
def show_section_tree(root):
@ -543,7 +553,10 @@ class TreeTab(Layout):
def show(self, screen_rect):
if not self._panel:
self._create_panel(screen_rect)
panel, body = screen_rect.hsplit(self.panel_width)
if self.place_right:
body, panel = screen_rect.hsplit(screen_rect.width - self.panel_width)
else:
panel, body = screen_rect.hsplit(self.panel_width)
self._resize_panel(panel)
self._panel.unhide()
@ -617,7 +630,7 @@ class TreeTab(Layout):
self.draw_panel()
def cmd_del_section(self, name):
"""Add named section to tree"""
"""Remove named section from tree"""
self._tree.del_section(name)
self.draw_panel()
@ -654,9 +667,9 @@ class TreeTab(Layout):
Parameters
==========
sorter : function with single arg returning string
sorter: function with single arg returning string
returns name of the section where window should be
create_sections :
create_sections:
if this parameter is True (default), if sorter returns unknown
section name it will be created dynamically
"""
@ -726,7 +739,10 @@ class TreeTab(Layout):
)
def layout(self, windows, screen_rect):
panel, body = screen_rect.hsplit(self.panel_width)
if self.place_right:
body, panel = screen_rect.hsplit(screen_rect.width - self.panel_width)
else:
panel, body = screen_rect.hsplit(self.panel_width)
self._resize_panel(panel)
Layout.layout(self, windows, body)

View File

@ -167,7 +167,7 @@ class MonadTall(_SimpleLayoutBase):
("change_ratio", .05, "Resize ratio"),
("change_size", 20, "Resize change in pixels"),
("new_client_position", "after_current",
"Place new windows : "
"Place new windows: "
" after_current - after the active window."
" before_current - before the active window,"
" top - at the top of the stack,"
@ -687,7 +687,8 @@ class MonadTall(_SimpleLayoutBase):
"""Get closest window to a point x,y"""
target = min(
clients,
key=lambda c: math.hypot(c.info()["x"] - x, c.info()["y"] - y)
key=lambda c: math.hypot(c.x - x, c.y - y),
default=self.clients.current_client
)
return target

View File

@ -17,8 +17,21 @@
# 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 annotations
from typing import Dict, Iterable, List, Optional, Set, Tuple, Union
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import (
Dict,
Iterable,
List,
Optional,
Set,
Tuple,
Union,
)
from libqtile.config import Match
from libqtile.command.client import InteractiveCommandClient
from libqtile.command.graph import (
@ -35,20 +48,42 @@ class LazyCall:
Parameters
----------
call : CommandGraphCall
call: CommandGraphCall
The call that is made
args : Tuple
args: Tuple
The args passed to the call when it is evaluated.
kwargs : Dict
kwargs: Dict
The kwargs passed to the call when it is evaluated.
"""
self._call = call
self._args = args
self._kwargs = kwargs
self._focused: Optional[Match] = None
self._layouts: Set[str] = set()
self._when_floating = True
def __call__(self, *args, **kwargs):
"""Convenience method to allow users to pass arguments to
functions decorated with `@lazy.function`.
@lazy.function
def my_function(qtile, pos_arg, keyword_arg=False):
pass
...
Key(... my_function("Positional argument", keyword_arg=True))
"""
# We need to return a new object so the arguments are not shared between
# a single instance of the LazyCall object.
return LazyCall(
self._call,
(*self._args, *args),
{**self._kwargs, **kwargs}
)
@property
def selectors(self) -> List[SelectorType]:
"""The selectors for the given call"""
@ -69,18 +104,23 @@ class LazyCall:
"""The kwargs to the given call"""
return self._kwargs
def when(self, layout: Optional[Union[Iterable[str], str]] = None,
def when(self, focused: Optional[Match] = None, layout: Optional[Union[Iterable[str], str]] = None,
when_floating: bool = True) -> 'LazyCall':
"""Enable call only for given layout(s) and floating state
Parameters
----------
layout : str, Iterable[str], or None
focused: Match or None
Match criteria to enable call for the current window
layout: str, Iterable[str], or None
Restrict call to one or more layouts.
If None, enable the call for all layouts.
when_floating : bool
when_floating: bool
Enable call when the current window is floating.
"""
if focused is not None:
self._focused = focused
if layout is not None:
self._layouts = {layout} if isinstance(layout, str) else set(layout)
@ -90,6 +130,9 @@ class LazyCall:
def check(self, q) -> bool:
cur_win_floating = q.current_window and q.current_window.floating
if self._focused and not self._focused.compare(q.current_window):
return False
if cur_win_floating and not self._when_floating:
return False

View File

@ -27,8 +27,7 @@ from typing import Any
try:
from dbus_next.aio import MessageBus
from dbus_next.service import ServiceInterface
from dbus_next.service import method, signal
from dbus_next.service import ServiceInterface, method, signal
has_dbus = True
except ImportError:
has_dbus = False

View File

@ -35,6 +35,9 @@ mod = "mod4"
terminal = guess_terminal()
keys = [
# A list of available commands that can be bound to keys can be found
# at https://docs.qtile.org/en/latest/manual/config/lazy.html
# Switch between windows
Key([mod], "h", lazy.layout.left(), desc="Move focus to left"),
Key([mod], "l", lazy.layout.right(), desc="Move focus to right"),
@ -76,7 +79,7 @@ keys = [
Key([mod], "Tab", lazy.next_layout(), desc="Toggle between layouts"),
Key([mod], "w", lazy.window.kill(), desc="Kill focused window"),
Key([mod, "control"], "r", lazy.restart(), desc="Restart Qtile"),
Key([mod, "control"], "r", lazy.reload_config(), desc="Reload the config"),
Key([mod, "control"], "q", lazy.shutdown(), desc="Shutdown Qtile"),
Key([mod], "r", lazy.spawncmd(),
desc="Spawn a command using a prompt widget"),
@ -143,6 +146,8 @@ screens = [
widget.QuickExit(),
],
24,
# border_width=[2, 0, 2, 0], # Draw top and bottom borders
# border_color=["ff00ff", "000000", "ff00ff", "000000"] # Borders are magenta
),
),
]

View File

@ -22,6 +22,7 @@ from typing import Dict, List
from libqtile import config, group, hook
from libqtile.backend.base import FloatStates
from libqtile.config import Match
class WindowVisibilityToggler:
@ -41,13 +42,13 @@ class WindowVisibilityToggler:
Parameters:
===========
scratchpad_name : string
scratchpad_name: string
The name (not label) of the ScratchPad group used to hide the window
window : window
window: window
The window to toggle
on_focus_lost_hide : bool
on_focus_lost_hide: bool
if True the associated window is hidden if it loses focus
warp_pointer : bool
warp_pointer: bool
if True the mouse pointer is warped to center of associated window
if shown. Only used if on_focus_lost_hide is True
"""
@ -111,7 +112,7 @@ class WindowVisibilityToggler:
# add hooks to determine if focus get lost
if self.on_focus_lost_hide:
if self.warp_pointer:
win.qtile.core.warp_pointer(win.x + win.width // 2, win.y + win.height // 2)
win.focus(warp=True)
hook.subscribe.client_focus(self.on_focus_change)
hook.subscribe.setgroup(self.on_focus_change)
@ -159,6 +160,8 @@ class DropDownToggler(WindowVisibilityToggler):
self.y = ddconfig.y
self.width = ddconfig.width
self.height = ddconfig.height
# Let's add the window to the scratchpad group.
window.togroup(scratchpad_name)
window.opacity = ddconfig.opacity
WindowVisibilityToggler.__init__(
self, scratchpad_name, window, ddconfig.on_focus_lost_hide, ddconfig.warp_pointer
@ -208,12 +211,13 @@ class ScratchPad(group._Group):
The ScratchPad, by default, has no label and thus is not shown in
GroupBox widget.
"""
def __init__(self, name='scratchpad', dropdowns: List[config.DropDown] = None, label=''):
def __init__(self, name='scratchpad', dropdowns: List[config.DropDown] = None, label='', single=False):
group._Group.__init__(self, name, label=label)
self._dropdownconfig = {dd.name: dd for dd in dropdowns} if dropdowns is not None else {}
self.dropdowns: Dict[str, DropDownToggler] = {}
self._spawned: Dict[int, str] = {}
self._spawned: Dict[str, Match] = {}
self._to_hide: List[str] = []
self._single = single
def _check_unsubscribe(self):
if not self.dropdowns:
@ -229,12 +233,12 @@ class ScratchPad(group._Group):
In case of a match the window gets associated to this DropDown object.
"""
name = ddconfig.name
if name not in self._spawned.values():
if name not in self._spawned:
if not self._spawned:
hook.subscribe.client_new(self.on_client_new)
cmd = self._dropdownconfig[name].command
pid = self.qtile.cmd_spawn(cmd)
self._spawned[pid] = name
pid = self.qtile.cmd_spawn(ddconfig.command)
self._spawned[name] = ddconfig.match or Match(net_wm_pid=pid)
def on_client_new(self, client, *args, **kwargs):
"""
@ -242,13 +246,23 @@ class ScratchPad(group._Group):
This method is subscribed if the given command is spawned
and unsubscribed immediately if the associated window is detected.
"""
client_pid = client.get_pid()
if client_pid in self._spawned:
name = self._spawned.pop(client_pid)
name = None
for n, match in self._spawned.items():
if match.compare(client):
name = n
break
if name is not None:
self._spawned.pop(name)
if not self._spawned:
hook.unsubscribe.client_new(self.on_client_new)
self.dropdowns[name] = DropDownToggler(client, self.name,
self._dropdownconfig[name])
self.dropdowns[name] = DropDownToggler(
client, self.name, self._dropdownconfig[name]
)
if self._single:
for n, d in self.dropdowns.items():
if n != name:
d.hide()
if name in self._to_hide:
self.dropdowns[name].hide()
self._to_hide.remove(name)
@ -289,12 +303,23 @@ class ScratchPad(group._Group):
"""
Toggle visibility of named DropDown.
"""
if self._single:
for n, d in self.dropdowns.items():
if n != name:
d.hide()
if name in self.dropdowns:
self.dropdowns[name].toggle()
else:
if name in self._dropdownconfig:
self._spawn(self._dropdownconfig[name])
def cmd_hide_all(self):
"""
Hide all scratchpads.
"""
for d in self.dropdowns.values():
d.hide()
def cmd_dropdown_reconfigure(self, name, **kwargs):
"""
reconfigure the named DropDown configuration.
@ -324,27 +349,46 @@ class ScratchPad(group._Group):
def get_state(self):
"""
Get the state of existing dropdown windows. Used for restoring state across
Qtile restarts.
Qtile restarts (`restart` == True) or config reloads (`restart` == False).
"""
state = []
for name, dd in self.dropdowns.items():
pid = dd.window.get_pid()
state.append((name, pid, dd.visible))
client_wid = dd.window.wid
state.append((name, client_wid, dd.visible))
return state
def restore_state(self, state):
def restore_state(self, state, restart: bool):
"""
Restore the state of existing dropdown windows. Used for restoring state across
Qtile restarts.
Qtile restarts (`restart` == True) or config reloads (`restart` == False).
"""
orphans = []
for name, pid, visible in state:
for name, wid, visible in state:
if name in self._dropdownconfig:
self._spawned[pid] = name
if not visible:
self._to_hide.append(name)
if restart:
self._spawned[name] = Match(wid=wid)
if not visible:
self._to_hide.append(name)
else:
# We are reloading the config; manage the clients now
self.dropdowns[name] = DropDownToggler(
self.qtile.windows_map[wid],
self.name,
self._dropdownconfig[name],
)
if not visible:
self.dropdowns[name].hide()
else:
orphans.append(pid)
orphans.append(wid)
if self._spawned:
# Handle re-managed clients after restarting
assert restart
hook.subscribe.client_new(self.on_client_new)
if not restart and self.dropdowns:
# We're only reloading so don't have these hooked via self.on_client_new
hook.subscribe.client_killed(self.on_client_killed)
hook.subscribe.float_change(self.on_float_change)
return orphans

View File

@ -162,7 +162,14 @@ def cmd_obj(args) -> None:
obj = get_object(cmd_client, args.obj_spec)
if args.function == "help":
print_commands("-o " + " ".join(args.obj_spec), obj)
try:
print_commands("-o " + " ".join(args.obj_spec), obj)
except CommandError:
if len(args.obj_spec) == 1:
print(f"{args.obj_spec} object needs a specified identifier e.g. '-o bar top'.")
sys.exit(1)
else:
raise
elif args.info:
print(args.function + get_formated_info(obj, args.function, args=True, short=False))
else:

View File

@ -6,10 +6,16 @@ from libqtile.log_utils import init_log
from libqtile.scripts import check, cmd_obj, migrate, run_cmd, shell, start, top
try:
import pkg_resources
VERSION = pkg_resources.require("qtile")[0].version
except (pkg_resources.DistributionNotFound, ImportError):
VERSION = 'dev'
# Python>3.7 can get the version from importlib
from importlib.metadata import distribution # type: ignore
VERSION = distribution("qtile").version
except ModuleNotFoundError:
try:
# pkg_resources is required for 3.7
import pkg_resources
VERSION = pkg_resources.require("qtile")[0].version
except (pkg_resources.DistributionNotFound, ModuleNotFoundError):
VERSION = 'dev'
def main():

View File

@ -21,6 +21,7 @@ import os
import os.path
import shutil
import sys
from functools import partial
from glob import glob
BACKUP_SUFFIX = ".migrate.bak"
@ -69,10 +70,32 @@ def pacman_to_checkupdates(query):
)
def reset_format(node, capture, filename):
args = capture.get("class_arguments")
if args:
if args[0].type == 260: # argument list
n_children = len(args[0].children)
for i in range(n_children):
# we only want to remove the format argument
if 'format' in str(args[0].children[i]):
# remove the argument and the trailing or preceeding comma
if i == n_children - 1: # last argument
args[0].children[i - 1].remove()
args[0].children[i - 1].remove()
else:
args[0].children[i].remove()
args[0].children[i].remove()
break
else: # there's only one argument
args[0].remove()
def bitcoin_to_crypto(query):
return (
query
.select_class("BitcoinTicker")
.modify(reset_format)
.rename("CryptoTicker")
)
@ -124,13 +147,24 @@ def new_at_current_to_new_client_position(query):
)
def windowtogroup_groupName_argument(funcname, query): # noqa: N802
return (
query
.select_method(funcname)
.modify_argument("groupName", "group_name")
)
MIGRATIONS = [
client_name_updated,
tile_master_windows_rename,
threaded_poll_text_rename,
pacman_to_checkupdates,
bitcoin_to_crypto,
hook_main_function,
new_at_current_to_new_client_position,
partial(windowtogroup_groupName_argument, "togroup"),
partial(windowtogroup_groupName_argument, "cmd_togroup"),
]

View File

@ -47,8 +47,8 @@ def run_cmd(opts) -> None:
rule_args = {"float": opts.float, "intrusive": opts.intrusive,
"group": opts.group, "break_on_match": not opts.dont_break}
cmd = root.call("add_rule")
_, rule_id = client.send((root.selectors, cmd.name, (match_args, rule_args), {}))
graph_cmd = root.call("add_rule")
_, rule_id = client.send((root.selectors, graph_cmd.name, (match_args, rule_args), {}))
def remove_rule() -> None:
cmd = root.call("remove_rule")

View File

@ -25,10 +25,10 @@ import fcntl
import inspect
import pprint
import re
import readline
import struct
import sys
import termios
from importlib import import_module
from typing import Any, List, Optional, Tuple
from libqtile.command.client import CommandClient
@ -54,12 +54,16 @@ class QSh:
"""Qtile shell instance"""
def __init__(self, client: CommandInterface, completekey="tab") -> None:
# Readline is imported here to prevent issues with terminal resizing
# which would result from readline being imported when qtile is first
# started
self.readline = import_module("readline")
self._command_client = CommandClient(client)
self._completekey = completekey
self._builtins = [i[3:] for i in dir(self) if i.startswith("do_")]
def complete(self, arg, state) -> Optional[str]:
buf = readline.get_line_buffer()
buf = self.readline.get_line_buffer()
completers = self._complete(buf, arg)
if completers and state < len(completers):
return completers[state]
@ -331,9 +335,9 @@ class QSh:
return "Invalid command: {}".format(line)
def loop(self) -> None:
readline.set_completer(self.complete)
readline.parse_and_bind(self._completekey + ": complete")
readline.set_completer_delims(" ()|")
self.readline.set_completer(self.complete)
self.readline.parse_and_bind(self._completekey + ": complete")
self.readline.set_completer_delims(" ()|")
while True:
try:

View File

@ -67,41 +67,44 @@ def shuffle_down(lst):
ColorType = Union[str, Tuple[int, int, int], Tuple[int, int, int, float]]
ColorsType = Union[ColorType, List[ColorType]]
def rgb(x):
def rgb(x: ColorType) -> Tuple[float, float, float, float]:
"""
Returns a valid RGBA tuple.
Here are some valid specifcations:
Here are some valid specifications:
#ff0000
with alpha: #ff000080
ff0000
with alpha: ff0000.5
(255, 0, 0)
with alpha: (255, 0, 0, 0.5)
Which is returned as (1.0, 0.0, 0.0, 0.5).
"""
if isinstance(x, (tuple, list)):
if len(x) == 4:
alpha = x[3]
alpha = x[-1]
else:
alpha = 1
alpha = 1.0
return (x[0] / 255.0, x[1] / 255.0, x[2] / 255.0, alpha)
elif isinstance(x, str):
if x.startswith("#"):
x = x[1:]
if "." in x:
x, alpha = x.split(".")
alpha = float("0." + alpha)
x, alpha_str = x.split(".")
alpha = float("0." + alpha_str)
else:
alpha = 1
alpha = 1.0
if len(x) not in (6, 8):
raise ValueError("RGB specifier must be 6 or 8 characters long.")
vals = [int(i, 16) for i in (x[0:2], x[2:4], x[4:6])]
vals = tuple(int(i, 16) for i in (x[0:2], x[2:4], x[4:6]))
if len(x) == 8:
alpha = int(x[6:8], 16) / 255.0
vals.append(alpha)
return rgb(vals)
vals += (alpha,) # type: ignore
return rgb(vals) # type: ignore
raise ValueError("Invalid RGB specifier.")
@ -110,7 +113,7 @@ def hex(x):
return '#%02x%02x%02x' % (int(r * 255), int(g * 255), int(b * 255))
def has_transparency(colour: Union[ColorType, List[ColorType]]):
def has_transparency(colour: ColorsType):
"""
Returns True if the colour is not fully opaque.
@ -124,13 +127,12 @@ def has_transparency(colour: Union[ColorType, List[ColorType]]):
return has_alpha(colour)
elif isinstance(colour, list):
print([c for c in colour])
return any([has_transparency(c) for c in colour])
return False
def remove_transparency(colour: Union[ColorType, List[ColorType]]):
def remove_transparency(colour: ColorsType):
"""
Returns a tuple of (r, g, b) with no alpha.
"""
@ -244,7 +246,7 @@ def send_notification(title, message, urgent=False, timeout=10000, id=None):
urgency = 2 if urgent else 1
try:
loop = asyncio.get_running_loop()
loop = asyncio.get_event_loop()
except RuntimeError:
logger.warning("Eventloop has not started. Cannot send notification.")
else:
@ -333,6 +335,7 @@ def scan_files(dirpath, *names):
['/wallpapers/w2.jpg', '/wallpapers/w3.jpg']})
"""
dirpath = os.path.expanduser(dirpath)
files = defaultdict(list)
for name in names:

View File

@ -78,12 +78,14 @@ widgets = {
"Sep": "sep",
"She": "she",
"Spacer": "spacer",
"StatusNotifier": "statusnotifier",
"StockTicker": "stock_ticker",
"SwapGraph": "graph",
"Systray": "systray",
"TaskList": "tasklist",
"TextBox": "textbox",
"ThermalSensor": "sensors",
"ThermalZone": "thermal_zone",
"Volume": "volume",
"VolumePulse": "volume_pulse",
"Wallpaper": "wallpaper",

View File

@ -61,8 +61,6 @@ class Backlight(base.InLoopPollText):
filenames = {} # type: Dict
orientations = base.ORIENTATION_HORIZONTAL
defaults = [
('backlight_name', 'acpi_video0', 'ACPI name of a backlight device'),
(
@ -100,6 +98,11 @@ class Backlight(base.InLoopPollText):
'Button5': partial(self.cmd_change_backlight, ChangeDirection.DOWN),
})
def finalize(self):
if self._future and not self._future.done():
self._future.cancel()
base.InLoopPollText.finalize(self)
def _load_file(self, path):
try:
with open(path, 'r') as f:

View File

@ -31,11 +31,14 @@
import asyncio
import copy
import math
import subprocess
from typing import Any, List, Tuple
from libqtile import bar, configurable, confreader
from libqtile.command import interface
from libqtile.command.base import CommandError, CommandObject, ItemT
from libqtile.lazy import LazyCall
from libqtile.log_utils import logger
@ -93,9 +96,12 @@ class _Widget(CommandObject, configurable.Configurable):
have been configured.
Callback functions can be assigned to button presses by passing a dict to the
'callbacks' kwarg. No arguments are passed to the callback function so, if
'callbacks' kwarg. No arguments are passed to the function so, if
you need access to the qtile object, it needs to be imported into your code.
``lazy`` functions can also be passed as callback functions and can be used in
the same was as keybindings.
For example:
.. code-block:: python
@ -105,18 +111,26 @@ class _Widget(CommandObject, configurable.Configurable):
def open_calendar():
qtile.cmd_spawn('gsimplecal next_month')
clock = widget.Clock(mouse_callbacks={'Button1': open_calendar})
clock = widget.Clock(
mouse_callbacks={
'Button1': open_calendar,
'Button3': lazy.spawn('gsimplecal prev_month')
}
)
When the clock widget receives a click with button 1, the ``open_calendar`` function
will be executed. Callbacks can be assigned to other buttons by adding more entries
to the passed dictionary.
will be executed.
"""
orientations = ORIENTATION_BOTH
offsetx: int = 0
offsety: int = 0
defaults = [
("background", None, "Widget background color"),
("mouse_callbacks", {}, "Dict of mouse button press callback functions."),
(
"mouse_callbacks",
{},
"Dict of mouse button press callback functions. Acceps functions and ``lazy`` calls."
),
] # type: List[Tuple[str, Any, str]]
def __init__(self, length, **config):
@ -134,11 +148,14 @@ class _Widget(CommandObject, configurable.Configurable):
if length in (bar.CALCULATED, bar.STRETCH):
self.length_type = length
self.length = 0
else:
assert isinstance(length, int)
elif isinstance(length, int):
self.length_type = bar.STATIC
self.length = length
else:
raise confreader.ConfigError("Widget width must be an int")
self.configured = False
self._futures: List[asyncio.TimerHandle] = []
@property
def length(self):
@ -154,12 +171,12 @@ class _Widget(CommandObject, configurable.Configurable):
def width(self):
if self.bar.horizontal:
return self.length
return self.bar.size
return self.bar.size - (self.bar.border_width[1] + self.bar.border_width[3])
@property
def height(self):
if self.bar.horizontal:
return self.bar.size
return self.bar.size - (self.bar.border_width[0] + self.bar.border_width[2])
return self.length
@property
@ -168,8 +185,6 @@ class _Widget(CommandObject, configurable.Configurable):
return self.offsetx
return self.offsety
# 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:
@ -189,6 +204,8 @@ class _Widget(CommandObject, configurable.Configurable):
pass
def _configure(self, qtile, bar):
self._test_orientation_compatibility(bar.horizontal)
self.qtile = qtile
self.bar = bar
self.drawer = bar.window.create_drawer(self.bar.width, self.bar.height)
@ -208,6 +225,8 @@ class _Widget(CommandObject, configurable.Configurable):
pass
def finalize(self):
for future in self._futures:
future.cancel()
if hasattr(self, 'layout') and self.layout:
self.layout.finalize()
self.drawer.finalize()
@ -234,7 +253,16 @@ class _Widget(CommandObject, configurable.Configurable):
def button_press(self, x, y, button):
name = 'Button{0}'.format(button)
if name in self.mouse_callbacks:
self.mouse_callbacks[name]()
cmd = self.mouse_callbacks[name]
if isinstance(cmd, LazyCall):
if cmd.check(self.qtile):
status, val = self.qtile.server.call(
(cmd.selectors, cmd.name, cmd.args, cmd.kwargs)
)
if status in (interface.ERROR, interface.EXCEPTION):
logger.error("Mouse callback command error %s: %s" % (cmd.name, val))
else:
cmd()
def button_release(self, x, y, button):
pass
@ -251,11 +279,15 @@ class _Widget(CommandObject, configurable.Configurable):
def _items(self, name: str) -> ItemT:
if name == "bar":
return True, []
elif name == "screen":
return True, []
return None
def _select(self, name, sel):
if name == "bar":
return self.bar
elif name == "screen":
return self.bar.screen
def cmd_info(self):
"""
@ -283,10 +315,14 @@ class _Widget(CommandObject, configurable.Configurable):
def timeout_add(self, seconds, method, method_args=()):
"""
This method calls either ``.call_later`` with given arguments.
This method calls ``.call_later`` with given arguments.
"""
return self.qtile.call_later(seconds, self._wrapper, method,
*method_args)
future = self.qtile.call_later(
seconds, self._wrapper, method, *method_args
)
self._futures.append(future)
return future
def call_process(self, command, **kwargs):
"""
@ -296,7 +332,15 @@ class _Widget(CommandObject, configurable.Configurable):
"""
return subprocess.check_output(command, **kwargs, encoding="utf-8")
def _remove_dead_timers(self):
"""Remove completed and cancelled timers from the list."""
self._futures = [
timer for timer in self._futures
if not (timer.cancelled() or timer.when() < self.qtile._eventloop.time())
]
def _wrapper(self, method, *method_args):
self._remove_dead_timers()
try:
method(*method_args)
except: # noqa: E722
@ -322,7 +366,7 @@ class _TextBox(_Widget):
"""
Base class for widgets that are just boxes containing text.
"""
orientations = ORIENTATION_HORIZONTAL
orientations = ORIENTATION_BOTH
defaults = [
("font", "sans", "Default font"),
("fontsize", None, "Font size. Calculated if None."),
@ -341,8 +385,8 @@ class _TextBox(_Widget):
def __init__(self, text=" ", width=bar.CALCULATED, **config):
self.layout = None
_Widget.__init__(self, width, **config)
self._text = text
self.add_defaults(_TextBox.defaults)
self.text = text
@property
def text(self):
@ -412,10 +456,16 @@ class _TextBox(_Widget):
def calculate_length(self):
if self.text:
return min(
self.layout.width,
self.bar.width
) + self.actual_padding * 2
if self.bar.horizontal:
return min(
self.layout.width,
self.bar.width
) + self.actual_padding * 2
else:
return min(
self.layout.width,
self.bar.height
) + self.actual_padding * 2
else:
return 0
@ -429,11 +479,32 @@ class _TextBox(_Widget):
if not self.can_draw():
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)
if self.bar.horizontal:
self.layout.draw(
self.actual_padding or 0,
int(self.bar.height / 2.0 - self.layout.height / 2.0) + 1
)
else:
# We need to do some transformations for vertical bars.
self.drawer.ctx.save()
# Left bar reads bottom to top
if self.bar.screen.left is self.bar:
self.drawer.ctx.rotate(-90 * math.pi / 180.0)
self.drawer.ctx.translate(-self.length, 0)
# Right bar is top to bottom
else:
self.drawer.ctx.translate(self.bar.width, 0)
self.drawer.ctx.rotate(90 * math.pi / 180.0)
self.layout.draw(
self.actual_padding or 0,
int(self.bar.width / 2.0 - self.layout.height / 2.0) + 1
)
self.drawer.ctx.restore()
self.drawer.draw(offsetx=self.offsetx, offsety=self.offsety, width=self.width)
def cmd_set_font(self, font=UNSPECIFIED, fontsize=UNSPECIFIED,
fontshadow=UNSPECIFIED):
@ -533,7 +604,7 @@ class ThreadPoolText(_TextBox):
"""
defaults = [
("update_interval", 600, "Update interval in seconds, if none, the "
"widget updates whenever it's done'."),
"widget updates whenever it's done."),
] # type: List[Tuple[str, Any, str]]
def __init__(self, text, **config):
@ -562,8 +633,8 @@ class ThreadPoolText(_TextBox):
else:
logger.warning('poll() returned None, not rescheduling')
future = self.qtile.run_in_executor(self.poll)
future.add_done_callback(on_done)
self.future = self.qtile.run_in_executor(self.poll)
self.future.add_done_callback(on_done)
def poll(self):
pass
@ -669,7 +740,7 @@ class Mirror(_Widget):
if self.reflects.drawer.needs_update:
self.drawer.clear(self.background or self.bar.background)
self.reflects.drawer.paint_to(self.drawer)
self.drawer.draw(offsetx=self.offset, width=self.width)
self.drawer.draw(offsetx=self.offset, offsety=self.offsety, width=self.width)
def button_press(self, x, y, button):
self.reflects.button_press(x, y, button)

View File

@ -42,7 +42,7 @@ from typing import Any, Dict, List, NamedTuple, Optional, Tuple
from libqtile import bar, configurable, images
from libqtile.images import Img
from libqtile.log_utils import logger
from libqtile.utils import send_notification
from libqtile.utils import ColorsType, send_notification
from libqtile.widget import base
@ -345,7 +345,9 @@ class BatteryHybrid(base.ThreadPoolText):
class Battery(base.ThreadPoolText):
"""A text-based battery monitoring widget currently supporting FreeBSD"""
orientations = base.ORIENTATION_HORIZONTAL
background: Optional[ColorsType]
low_background: Optional[ColorsType]
defaults = [
('charge_char', '^', 'Character to indicate the battery is charging'),
('discharge_char', 'V', 'Character to indicate the battery is discharging'),
@ -357,6 +359,7 @@ class Battery(base.ThreadPoolText):
('show_short_text', True, 'Show "Full" or "Empty" rather than formated text'),
('low_percentage', 0.10, "Indicates when to use the low_foreground color 0 < x < 1"),
('low_foreground', 'FF0000', 'Font color on low battery'),
('low_background', None, 'Background color on low battery'),
('update_interval', 60, 'Seconds between status updates'),
('battery', 0, 'Which battery should be monitored (battery number or name)'),
('notify_below', None, 'Send a notification below this battery level.'),
@ -374,6 +377,10 @@ class Battery(base.ThreadPoolText):
self._battery = self._load_battery(**config)
self._has_notified = False
if not self.low_background:
self.low_background = self.background
self.normal_background = self.background
@staticmethod
def _load_battery(**config):
"""Function used to load the Battery object
@ -425,8 +432,10 @@ class Battery(base.ThreadPoolText):
if self.layout is not None:
if status.state == BatteryState.DISCHARGING and status.percent < self.low_percentage:
self.layout.colour = self.low_foreground
self.background = self.low_background
else:
self.layout.colour = self.foreground
self.background = self.normal_background
if status.state == BatteryState.CHARGING:
char = self.charge_char
@ -541,7 +550,7 @@ class BatteryIcon(base._Widget):
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)
self.drawer.draw(offsetx=self.offset, offsety=self.offsety, width=self.length)
@staticmethod
def _get_icon_key(status: BatteryStatus) -> str:

View File

@ -40,8 +40,6 @@ class Bluetooth(base._TextBox):
.. _dbus-next: https://pypi.org/project/dbus-next/
"""
orientations = base.ORIENTATION_HORIZONTAL
defaults = [
('hci', '/dev_XX_XX_XX_XX_XX_XX', 'hci0 device path, can be found with d-feet or similar dbus explorer.')
]

View File

@ -30,7 +30,6 @@ from libqtile.widget import base
class Canto(base.ThreadPoolText):
"""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"),

View File

@ -27,8 +27,6 @@ from libqtile.widget import base
class CapsNumLockIndicator(base.ThreadPoolText):
"""Really simple widget to show the current Caps/Num Lock state."""
orientations = base.ORIENTATION_HORIZONTAL
defaults = [('update_interval', 0.5, 'Update Time in seconds.')]
def __init__(self, **config):
@ -40,7 +38,8 @@ class CapsNumLockIndicator(base.ThreadPoolText):
try:
output = self.call_process(['xset', 'q'])
except subprocess.CalledProcessError as err:
output = err.output.decode()
output = err.output
return []
if output.startswith("Keyboard"):
indicators = re.findall(r"(Caps|Num)\s+Lock:\s*(\w*)", output)
return indicators

View File

@ -27,7 +27,6 @@ from libqtile.widget import base
class CheckUpdates(base.ThreadPoolText):
"""Shows number of pending updates in different unix systems"""
orientations = base.ORIENTATION_HORIZONTAL
defaults = [
("distro", "Arch", "Name of your distribution"),
("custom_command", None, "Custom shell command for checking updates (counts the lines of the output)"),

View File

@ -27,7 +27,6 @@ from libqtile.widget import base
class Chord(base._TextBox):
"""Display current key chord"""
orientations = base.ORIENTATION_HORIZONTAL
defaults = [
("chords_colors", {},
"colors per chord in form of tuple ('bg', 'fg')."),

View File

@ -28,7 +28,6 @@ from libqtile.widget import base
class Clipboard(base._TextBox):
"""Display current clipboard contents"""
orientations = base.ORIENTATION_HORIZONTAL
defaults = [
("selection", "CLIPBOARD",
"the selection to display(CLIPBOARD or PRIMARY)"),
@ -66,7 +65,7 @@ class Clipboard(base._TextBox):
if owner_id in self.qtile.windows_map:
owner = self.qtile.windows_map[owner_id].window
else:
owner = xcbq.Window(self.qtile.core.conn, owner_id)
owner = xcbq.window.XWindow(self.qtile.core.conn, owner_id)
owner_class = owner.get_wm_class()
if owner_class:

View File

@ -41,7 +41,6 @@ except ImportError:
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'),

View File

@ -33,7 +33,6 @@ class Cmus(base.ThreadPoolText):
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.'),
@ -57,7 +56,7 @@ class Cmus(base.ThreadPoolText):
try:
output = self.call_process(['cmus-remote', '-C', 'status'])
except subprocess.CalledProcessError as err:
output = err.output.decode()
output = err.output
if output.startswith("status"):
output = output.splitlines()
info = {'status': "",

View File

@ -28,13 +28,12 @@ from libqtile.widget 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"),
('date', datetime.now(), "The datetime for the end of the countdown"),
]
def __init__(self, **config):

View File

@ -31,8 +31,6 @@ class CPU(base.ThreadPoolText):
.. _psutil: https://pypi.org/project/psutil/
"""
orientations = base.ORIENTATION_HORIZONTAL
defaults = [
("update_interval", 1.0, "Update interval for the CPU widget"),
(

View File

@ -41,8 +41,6 @@ class _CrashMe(base._TextBox):
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)

Some files were not shown because too many files have changed in this diff Show More