Merge branch 'master' of https://github.com/qtile/qtile into qtile-master
commit
f6c9637651
|
@ -1,2 +1,2 @@
|
|||
github: [ramnes]
|
||||
custom: ["https://paypal.me/ramnes"]
|
||||
github: ["m-col"]
|
||||
custom: ["https://liberapay.com/mcol"]
|
||||
|
|
|
@ -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.
|
||||
-->
|
|
@ -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
|
|
@ -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
|
||||
|
|
35
CHANGELOG
35
CHANGELOG
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
12
README.rst
12
README.rst
|
@ -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
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
|
30
docs/conf.py
30
docs/conf.py
|
@ -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 = {}
|
||||
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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"])
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
@ -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()")
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
.. _default_config:
|
||||
|
||||
===================
|
||||
Default Config File
|
||||
===================
|
||||
|
||||
.. literalinclude:: ../../../libqtile/resources/default_config.py
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
--------------------------
|
||||
|
|
|
@ -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?
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
]
|
||||
|
|
|
@ -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
|
||||
============
|
||||
|
|
|
@ -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
|
||||
---------------------
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
==================
|
||||
|
||||
|
|
|
@ -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.
|
|
@ -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::
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
.. _ref-extensions:
|
||||
|
||||
===================
|
||||
Built-in Extensions
|
||||
===================
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
=========
|
||||
Reference
|
||||
=========
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
hooks
|
||||
layouts
|
||||
widgets
|
||||
extensions
|
|
@ -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``
|
||||
=======================
|
||||
|
||||
|
|
|
@ -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
|
|
@ -1,6 +1,7 @@
|
|||
setuptools_scm
|
||||
sphinx
|
||||
sphinx_rtd_theme
|
||||
funcparserlib==1.0.0a0
|
||||
sphinxcontrib-seqdiag
|
||||
numpydoc
|
||||
|
||||
|
|
|
@ -63,7 +63,7 @@ qtile_class_template = Template('''
|
|||
{% for key, default, description in defaults %}
|
||||
* - ``{{ key }}``
|
||||
- ``{{ default }}``
|
||||
- {{ description }}
|
||||
- {{ description[1:-1] }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if commandable %}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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
|
||||
|
|
233
libqtile/bar.py
233
libqtile/bar.py
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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 ""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"] = []
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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"),
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.')
|
||||
]
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)"),
|
||||
|
|
|
@ -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')."),
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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': "",
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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"),
|
||||
(
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue