Skip to content

Commit

Permalink
Merge 62f82be into 8d254f6
Browse files Browse the repository at this point in the history
  • Loading branch information
elParaguayo committed May 1, 2024
2 parents 8d254f6 + 62f82be commit b34e167
Show file tree
Hide file tree
Showing 11 changed files with 289 additions and 18 deletions.
1 change: 1 addition & 0 deletions docs/manual/config/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ mix!)
mouse
screens
hooks
match

In addition to the above variables, there are several other boolean
configuration variables that control specific aspects of Qtile's behavior:
Expand Down
85 changes: 85 additions & 0 deletions docs/manual/config/match.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
.. _match:

================
Matching windows
================

Qtile's config provides a number of situations where the behaviour depends
on whether the relevant window matches some specified criteria.

These situations include:

- Defining which windows should be floated by default
- Assigning windows to specific groups
- Assigning window to a master section of a layout

In each instance, the criteria are defined via a ``Match`` object. The properties
of the object will be compared to a :class:`~libqtile.base.Window` to determine if
its properties *match*. It can match by title, wm_class, role, wm_type,
wm_instance_class, net_wm_pid, or wid. Additionally, a function may be
passed, which takes in the :class:`~libqtile.base.Window` to be compared
against and returns a boolean.

A basic rule would therefore look something like:

.. code:: python
Match(wm_class="mpv")
This would match against any window whose class was ``mpv``.

Where a string is provided as an argument then the value must match exactly. More
flexibility can be achieved by using regular expressions. For example:

.. code:: python
import re
Match(re.compile(wm_class=r"mpv"))
This would still match a window whose class was ``mpv`` but it would also match
any class starting with ``mpv`` e.g. ``mpvideo``.

.. note::

When providing a regular expression, qtile applies the ``.match`` method.
This matches from the start of the string so, if you want to match any substring,
you will need to adapt the regular expression accordingly e.g.

.. code:: python
import re
Match(wm_class=re.compile(r".*mpv"))
This would match any string containing ``mpv``

Creating advanced rules
=======================

While the ``func`` parameter allows users to create more complex matches, this requires
a knowledge of qtile's internal objects. An alternative is to combine Match objects using
logical operators ``& `` (and), ``|`` (or), ``~`` (not) and ``^`` (xor).

For example, to create rule that matches all windows with a fixed aspect ratio except for
mpv windows, you would provide the following:

.. code:: python
Match(func=lambda c: c.has_fixed_ratio()) & ~Match(wm_class="mpv")
It is also possible to use wrappers for ``Match`` objects if you do not want to use the
operators. The following wrappers are available:

- ``MatchAll(Match(...), ...)`` equivalent to "and" test. All matches must match.
- ``MatchAny(Match(...), ...)`` equivalent to "or" test. At least one match must match.
- ``MatchOnlyOne(Match(...), Match(...))`` equivalent to "xor". Only one match must match.
- ``InvertMatch(Match(...))`` equivalent to "not". Inverts the result of the match.

So, to recreate the above rule using the wrappers, you would write the following:

.. code:: python
from libqtile.config import InvertMatch, Match, MatchAll
MatchAll(Match(func=lambda c: c.has_fixed_ratio()), InvertMatch(Match(wm_class="mpv")))
2 changes: 1 addition & 1 deletion libqtile/backend/base/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ def wants_to_fullscreen(self) -> bool:
"""Does this window want to be fullscreen?"""
return False

def match(self, match: config.Match) -> bool:
def match(self, match: config._Match) -> bool:
"""Compare this window against a Match instance."""
return match.compare(self)

Expand Down
2 changes: 1 addition & 1 deletion libqtile/backend/wayland/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -604,7 +604,7 @@ def info(self) -> dict:
fullscreen=self._float_state == FloatStates.FULLSCREEN,
)

def match(self, match: config.Match) -> bool:
def match(self, match: config._Match) -> bool:
return match.compare(self)

def add_idle_inhibitor(
Expand Down
84 changes: 81 additions & 3 deletions libqtile/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -832,7 +832,85 @@ def convert_deprecated_list(vals: list[str], name: str) -> re.Pattern:
return re.compile(regex_input)


class Match:
class _Match:
"""Base class to implement bitwise logic methods for Match objects."""

def compare(self, client: base.Window) -> bool:
return True

def __invert__(self) -> InvertMatch:
return InvertMatch(self)

def __and__(self, other: _Match) -> MatchAll:
if not isinstance(other, _Match):
raise TypeError

return MatchAll(self, other)

def __or__(self, other: _Match) -> MatchAny:
if not isinstance(other, _Match):
raise TypeError

return MatchAny(self, other)

def __xor__(self, other: _Match) -> MatchOnlyOne:
if not isinstance(other, _Match):
raise TypeError

return MatchOnlyOne(self, other)


class InvertMatch(_Match):
"""Wrapper to invert the result of the comparison."""

def __init__(self, match: _Match):
self.match = match

def compare(self, client: base.Window) -> bool:
return not self.match.compare(client)

def __repr__(self) -> str:
return "<InvertMatch(%r)>" % self.match


class MatchAll(_Match):
"""Wrapper to check if all comparisons return True."""

def __init__(self, *matches: _Match):
self.matches = matches

def compare(self, client: base.Window) -> bool:
return all(m.compare(client) for m in self.matches)

def __repr__(self) -> str:
return "<MatchAll(%r)>" % (self.matches,)


class MatchAny(MatchAll):
"""Wrapper to check if at least one of the comparisons returns True."""

def compare(self, client: base.Window) -> bool:
return any(m.compare(client) for m in self.matches)

def __repr__(self) -> str:
return "<MatchAny(%r)>" % (self.matches,)


class MatchOnlyOne(_Match):
"""Wrapper to check if only one of the two comparisons returns True."""

def __init__(self, match1: _Match, match2: _Match):
self.match1 = match1
self.match2 = match2

def compare(self, client: base.Window) -> bool:
return self.match1.compare(client) != self.match2.compare(client)

def __repr__(self) -> str:
return "<MatchOnlyOne(%r, %r)>" % (self.match1, self.match2)


class Match(_Match):
"""
Window properties to compare (match) with a window.
Expand Down Expand Up @@ -1002,13 +1080,13 @@ class Rule:

def __init__(
self,
match: Match | list[Match],
match: _Match | list[_Match],
group: _Group | None = None,
float: bool = False,
intrusive: bool = False,
break_on_match: bool = True,
) -> None:
if isinstance(match, Match):
if isinstance(match, _Match):
self.matchlist = [match]
else:
self.matchlist = match
Expand Down
6 changes: 3 additions & 3 deletions libqtile/layout/floating.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from typing import TYPE_CHECKING

from libqtile.command.base import expose_command
from libqtile.config import Match
from libqtile.config import Match, _Match
from libqtile.layout.base import Layout

if TYPE_CHECKING:
Expand All @@ -47,7 +47,7 @@ class Floating(Layout):
Floating layout, which does nothing with windows but handles focus order
"""

default_float_rules = [
default_float_rules: list[_Match] = [
Match(wm_type="utility"),
Match(wm_type="notification"),
Match(wm_type="toolbar"),
Expand All @@ -74,7 +74,7 @@ class Floating(Layout):
]

def __init__(
self, float_rules: list[Match] | None = None, no_reposition_rules=None, **config
self, float_rules: list[_Match] | None = None, no_reposition_rules=None, **config
):
"""
If you have certain apps that you always want to float you can provide
Expand Down
6 changes: 3 additions & 3 deletions libqtile/layout/screensplit.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

from libqtile import hook
from libqtile.command.base import expose_command
from libqtile.config import Match, ScreenRect
from libqtile.config import ScreenRect, _Match
from libqtile.layout import Columns, Max
from libqtile.layout.base import Layout
from libqtile.log_utils import logger
Expand All @@ -39,7 +39,7 @@

class Split:
def __init__(
self, *, name: str, rect: Rect, layout: Layout, matches: list[Match] = list()
self, *, name: str, rect: Rect, layout: Layout, matches: list[_Match] = list()
) -> None:
# Check that rect is correctly defined
if not isinstance(rect, (tuple, list)):
Expand All @@ -53,7 +53,7 @@ def __init__(

if matches:
if isinstance(matches, list):
if not all(isinstance(m, Match) for m in matches):
if not all(isinstance(m, _Match) for m in matches):
raise ValueError("Invalid object in 'matches'.")
else:
raise ValueError("'matches' must be a list of 'Match' objects.")
Expand Down
4 changes: 2 additions & 2 deletions libqtile/layout/tile.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
from typing import TYPE_CHECKING

from libqtile.command.base import expose_command
from libqtile.config import Match
from libqtile.config import _Match
from libqtile.layout.base import _SimpleLayoutBase

if TYPE_CHECKING:
Expand Down Expand Up @@ -150,7 +150,7 @@ def reset_master(self, match=None):
return
if self.clients:
master_match = match or self.master_match
if isinstance(master_match, Match):
if isinstance(master_match, _Match):
master_match = [master_match]
masters = []
for c in self.clients:
Expand Down
6 changes: 3 additions & 3 deletions libqtile/lazy.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
from typing import Callable, Iterable

from libqtile.command.graph import SelectorType
from libqtile.config import Match
from libqtile.config import _Match


class LazyCall:
Expand All @@ -50,7 +50,7 @@ def __init__(self, call: CommandGraphCall, args: tuple, kwargs: dict) -> None:
self._args = args
self._kwargs = kwargs

self._focused: Match | None = None
self._focused: _Match | None = None
self._if_no_focused: bool = False
self._layouts: set[str] = set()
self._when_floating = True
Expand Down Expand Up @@ -96,7 +96,7 @@ def kwargs(self) -> dict:

def when(
self,
focused: Match | None = None,
focused: _Match | None = None,
if_no_focused: bool = False,
layout: Iterable[str] | str | None = None,
when_floating: bool = True,
Expand Down
4 changes: 2 additions & 2 deletions libqtile/scratchpad.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from libqtile import config, group, hook
from libqtile.backend.base import FloatStates
from libqtile.command.base import expose_command
from libqtile.config import Match
from libqtile.config import Match, _Match

if TYPE_CHECKING:
from libqtile.backend.base import Window
Expand Down Expand Up @@ -243,7 +243,7 @@ def __init__(
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[str, Match] = {}
self._spawned: dict[str, _Match] = {}
self._to_hide: list[str] = []
self._single = single

Expand Down

0 comments on commit b34e167

Please sign in to comment.