# Copyright (c) 2017 numirias
# Copyright (c) 2024 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
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import copy
import time
from enum import Enum, Flag, auto
from math import isclose
from typing import NamedTuple
from libqtile import hook
from libqtile.backend.base import Window
from libqtile.command.base import expose_command
from libqtile.hook import Hook, qtile_hooks
from libqtile.layout.base import Layout
plasma_hook = Hook(
"plasma_add_mode",
"""
Used to flag when the add mode of the Plasma layout has changed.
The hooked function should take one argument being the layout object.
""",
)
qtile_hooks.register_hook(plasma_hook)
class NotRestorableError(Exception):
pass
class Point(NamedTuple):
x: int
y: int
class Dimensions(NamedTuple):
x: int
y: int
width: int
height: int
class Orient(Flag):
HORIZONTAL = 0
VERTICAL = 1
class Direction(Enum):
UP = auto()
DOWN = auto()
LEFT = auto()
RIGHT = auto()
@property
def orient(self):
return Orient.HORIZONTAL if self in [self.LEFT, self.RIGHT] else Orient.VERTICAL
@property
def offset(self):
return 1 if self in [self.RIGHT, self.DOWN] else -1
class Priority(Enum):
FIXED = auto()
BALANCED = auto()
class AddMode(Flag):
HORIZONTAL = auto()
VERTICAL = auto()
SPLIT = auto()
@property
def orient(self):
return Orient.VERTICAL if self & self.VERTICAL else Orient.HORIZONTAL
border_check = {
Direction.UP: lambda a, b: isclose(a.y, b.y_end),
Direction.DOWN: lambda a, b: isclose(a.y_end, b.y),
Direction.LEFT: lambda a, b: isclose(a.x, b.x_end),
Direction.RIGHT: lambda a, b: isclose(a.x_end, b.x),
}
def flatten(value):
"""Flattens a nested list of lists into a single list."""
out = []
for x in value:
if not isinstance(x, list):
out.append(x)
else:
out.extend(flatten(x))
return out
class Node:
"""
A tree node.
Each node represents a container that can hold a payload and child nodes.
"""
min_size_default = 100
root_orient = Orient.HORIZONTAL
priority = Priority.FIXED
def __init__(self, payload=None, x=None, y=None, width=None, height=None):
self.payload = payload
self._x = x
self._y = y
self._width = width
self._height = height
self._size = None
self.children = []
self.last_accessed = 0
self.parent = None
self.restorables = {}
def __repr__(self):
info = self.payload or ""
if self:
info += " +%d" % len(self)
return f"<Node {info} {id(self):x}>"
# Define dunder methods to treat Node objects like an iterable
def __contains__(self, node):
if node is self:
return True
for child in self:
if node in child:
return True
return False
def __iter__(self):
yield from self.children
def __getitem__(self, key):
return self.children[key]
def __setitem__(self, key, value):
self.children[key] = value
def __len__(self):
return len(self.children)
@property
def root(self):
try:
# Walk way up the tree until we find the root
return self.parent.root
except AttributeError:
# Node has no parent (self.parent is None) so node must be root
return self
@property
def is_root(self):
return self.parent is None
@property
def is_leaf(self):
return not self.children
@property
def index(self):
return self.parent.children.index(self)
@property
def tree(self):
return [c.tree if c else c for c in self]
@property
def siblings(self):
if self.is_root:
return list()
return [c for c in self.parent if c is not self]
@property
def first_leaf(self):
if self.is_leaf:
return self
return self[0].first_leaf
@property
def last_leaf(self):
if self.is_leaf:
return self
return self[-1].last_leaf
@property
def recent_leaf(self):
if self.is_leaf:
return self
return max(self, key=lambda n: n.last_accessed).recent_leaf
@property
def prev_leaf(self):
if self.is_root:
return self.last_leaf
idx = self.index - 1
if idx < 0:
return self.parent.prev_leaf
return self.parent[idx].last_leaf
@property
def next_leaf(self):
if self.is_root:
return self.first_leaf
idx = self.index + 1
if idx >= len(self.parent):
return self.parent.next_leaf
return self.parent[idx].first_leaf
@property
def all_leafs(self):
if self.is_leaf:
yield self
for child in self:
yield from child.all_leafs
@property
def orient(self):
if self.is_root:
return self.root_orient
return ~self.parent.orient
@property
def horizontal(self):
return self.orient is Orient.HORIZONTAL
@property
def vertical(self):
return self.orient is Orient.VERTICAL
@property
def x(self):
if self.is_root:
return self._x
if self.horizontal:
return self.parent.x
return self.parent.x + self.size_offset
@x.setter
def x(self, val):
if not self.is_root:
return
self._x = val
@property
def y(self):
if self.is_root:
return self._y
if self.vertical:
return self.parent.y
return self.parent.y + self.size_offset
@y.setter
def y(self, val):
if not self.is_root:
return
self._y = val
@property
def pos(self):
return Point(self.x, self.y)
@property
def width(self):
if self.is_root:
return self._width
if self.horizontal:
return self.parent.width
return self.size
@width.setter
def width(self, val):
if self.is_root:
self._width = val
elif self.horizontal:
self.parent.size = val
else:
self.size = val
@property
def height(self):
if self.is_root:
return self._height
if self.vertical:
return self.parent.height
return self.size
@height.setter
def height(self, val):
if self.is_root:
self._height = val
elif self.vertical:
self.parent.size = val
else:
self.size = val
@property
def x_end(self):
return self.x + self.width
@property
def y_end(self):
return self.y + self.height
@property
def x_center(self):
return self.x + self.width / 2
@property
def y_center(self):
return self.y + self.height / 2
@property
def center(self):
return Point(self.x_center, self.y_center)
@property
def pixel_perfect(self):
"""
Return pixel-perfect int dimensions (x, y, width, height) which
compensate for gaps in the layout grid caused by plain int conversions.
"""
x, y, width, height = self.x, self.y, self.width, self.height
threshold = 0.99999
if (x - int(x)) + (width - int(width)) > threshold:
width += 1
if (y - int(y)) + (height - int(height)) > threshold:
height += 1
return Dimensions(*map(int, (x, y, width, height)))
@property
def capacity(self):
return self.width if self.horizontal else self.height
@property
def size(self):
"""Return amount of space taken in parent container."""
if self.is_root:
return None
if self.fixed:
return self._size
if self.flexible:
# Distribute space evenly among flexible nodes
taken = sum(n.size for n in self.siblings if not n.flexible)
flexibles = [n for n in self.parent if n.flexible]
return (self.parent.capacity - taken) / len(flexibles)
return max(sum(gc.min_size for gc in c) for c in self)
@size.setter
def size(self, val):
if self.is_root or not self.siblings:
return
if val is None:
self.reset_size()
return
occupied = sum(s.min_size_bound for s in self.siblings)
val = max(min(val, self.parent.capacity - occupied), self.min_size_bound)
self.force_size(val)
def force_size(self, val):
"""Set size without considering available space."""
Node.fit_into(self.siblings, self.parent.capacity - val)
if val == 0:
return
if self:
Node.fit_into([self], val)
self._size = val
@property
def size_offset(self):
return sum(c.size for c in self.parent[: self.index])
@staticmethod
def fit_into(nodes, space):
"""Resize nodes to fit them into the available space."""
if not nodes:
return
occupied = sum(n.min_size for n in nodes)
if space >= occupied and any(n.flexible for n in nodes):
# If any flexible node exists, it will occupy the space
# automatically, not requiring any action.
return
nodes_left = nodes[:]
space_left = space
if space < occupied:
for node in nodes:
if node.min_size_bound != node.min_size:
continue
# Substract nodes that are already at their minimal possible
# size because they can't be shrinked any further.
space_left -= node.min_size
nodes_left.remove(node)
if not nodes_left:
return
factor = space_left / sum(n.size for n in nodes_left)
for node in nodes_left:
new_size = node.size * factor
if node.fixed:
node._size = new_size # pylint: disable=protected-access
for child in node:
Node.fit_into(child, new_size)
@property
def fixed(self):
"""A node is fixed if it has a specified size."""
return self._size is not None
@property
def min_size(self):
if self.fixed:
return self._size
if self.is_leaf:
return self.min_size_default
size = max(sum(gc.min_size for gc in c) for c in self)
return max(size, self.min_size_default)
@property
def min_size_bound(self):
if self.is_leaf:
return self.min_size_default
return max(sum(gc.min_size_bound for gc in c) or self.min_size_default for c in self)
def reset_size(self):
self._size = None
@property
def flexible(self):
"""
A node is flexible if its size isn't (explicitly or implicitly)
determined.
"""
if self.fixed:
return False
return all((any(gc.flexible for gc in c) or c.is_leaf) for c in self)
def access(self):
self.last_accessed = time.time()
try:
self.parent.access()
except AttributeError:
pass
def neighbor(self, direction):
"""Return adjacent leaf node in specified direction."""
if self.is_root:
return None
if direction.orient is self.parent.orient:
target_idx = self.index + direction.offset
if 0 <= target_idx < len(self.parent):
return self.parent[target_idx].recent_leaf
if self.parent.is_root:
return None
return self.parent.parent.neighbor(direction)
return self.parent.neighbor(direction)
@property
def up(self):
return self.neighbor(Direction.UP)
@property
def down(self):
return self.neighbor(Direction.DOWN)
@property
def left(self):
return self.neighbor(Direction.LEFT)
@property
def right(self):
return self.neighbor(Direction.RIGHT)
def common_border(self, node, direction):
"""Return whether a common border with given node in specified
direction exists.
"""
if not border_check[direction](self, node):
return False
if direction in [Direction.UP, Direction.DOWN]:
detached = node.x >= self.x_end or node.x_end <= self.x
else:
detached = node.y >= self.y_end or node.y_end <= self.y
return not detached
def close_neighbor(self, direction):
"""Return visually adjacent leaf node in specified direction."""
nodes = [n for n in self.root.all_leafs if self.common_border(n, direction)]
if not nodes:
return None
most_recent = max(nodes, key=lambda n: n.last_accessed)
if most_recent.last_accessed > 0:
return most_recent
if direction in [Direction.UP, Direction.DOWN]:
match = lambda n: n.x <= self.x_center <= n.x_end # noqa: E731
else:
match = lambda n: n.y <= self.y_center <= n.y_end # noqa: E731
return next(n for n in nodes if match(n))
@property
def close_up(self):
return self.close_neighbor(Direction.UP)
@property
def close_down(self):
return self.close_neighbor(Direction.DOWN)
@property
def close_left(self):
return self.close_neighbor(Direction.LEFT)
@property
def close_right(self):
return self.close_neighbor(Direction.RIGHT)
def add_child(self, node, idx=None):
if idx is None:
idx = len(self)
self.children.insert(idx, node)
node.parent = self
if len(self) == 1:
return
total = self.capacity
if Node.priority is Priority.FIXED:
# Prioritising windows with fixed sizes means the most space the siblings
# must fit into is total width less the minimum size for a new node.
# However, the new node doesn't have a fixed size so will expand to fit
# available space
space = total - Node.min_size_default
else:
# Balanced approach means that space for existing nodes is reduced so that
# all nodes would be distributed evenly if none had fixed widths
space = total - (total / len(self))
Node.fit_into(node.siblings, space)
def add_child_after(self, new, old):
self.add_child(new, idx=old.index + 1)
def remove_child(self, node):
node._save_restore_state() # pylint: disable=W0212
node.force_size(0)
self.children.remove(node)
if len(self) == 1:
child = self[0]
if self.is_root:
# A single child doesn't need a fixed size
child.reset_size()
else:
# Collapse tree with a single child
self.parent.replace_child(self, child)
Node.fit_into(child, self.capacity)
def remove(self):
self.parent.remove_child(self)
def replace_child(self, old, new):
self[old.index] = new
new.parent = self
new._size = old._size # pylint: disable=protected-access
def flip_with(self, node, reverse=False):
"""Join with node in a new, orthogonal container."""
container = Node()
self.parent.replace_child(self, container)
self.reset_size()
for child in [node, self] if reverse else [self, node]:
container.add_child(child)
def add_node(self, node, mode=None):
"""Add node according to the mode.
This can result in adding it as a child, joining with it in a new
flipped sub-container, or splitting the space with it.
"""
if self.is_root:
self.add_child(node)
elif mode is None:
self.parent.add_child_after(node, self)
elif mode.orient is self.parent.orient:
if mode & AddMode.SPLIT:
node._size = 0 # pylint: disable=protected-access
self.parent.add_child_after(node, self)
self._size = node._size = self.size / 2
else:
self.parent.add_child_after(node, self)
else:
self.flip_with(node)
def restore(self, node):
"""Restore node.
Try to add the node in a place where a node with the same payload
has previously been.
"""
restorables = self.root.restorables
try:
parent, idx, sizes, fixed, flip = restorables[node.payload]
except KeyError:
raise NotRestorableError() # pylint: disable=raise-missing-from
if parent not in self.root:
# Don't try to restore if parent is not part of the tree anymore
raise NotRestorableError()
node.reset_size()
if flip:
old_parent_size = parent.size
parent.flip_with(node, reverse=(idx == 0))
node.size, parent.size = sizes
Node.fit_into(parent, old_parent_size)
else:
parent.add_child(node, idx=idx)
node.size = sizes[0]
if len(sizes) == 2:
node.siblings[0].size = sizes[1]
if not fixed:
node.reset_size()
del restorables[node.payload]
def _save_restore_state(self):
parent = self.parent
sizes = (self.size,)
flip = False
if len(self.siblings) == 1:
# If there is only one node left in the container, we need to save
# its size too because the size will be lost.
sizes += (self.siblings[0]._size,) # pylint: disable=W0212
if not self.parent.is_root:
flip = True
parent = self.siblings[0]
self.root.restorables[self.payload] = (parent, self.index, sizes, self.fixed, flip)
def move(self, direction):
"""Move this node in `direction`. Return whether node was moved."""
if self.is_root:
return False
if direction.orient is self.parent.orient:
old_idx = self.index
new_idx = old_idx + direction.offset
if 0 <= new_idx < len(self.parent):
p = self.parent
p[old_idx], p[new_idx] = p[new_idx], p[old_idx]
return True
new_sibling = self.parent.parent
else:
new_sibling = self.parent
try:
new_parent = new_sibling.parent
idx = new_sibling.index
except AttributeError:
return False
self.reset_size()
self.parent.remove_child(self)
new_parent.add_child(self, idx + (1 if direction.offset == 1 else 0))
return True
def move_up(self):
return self.move(Direction.UP)
def move_down(self):
return self.move(Direction.DOWN)
def move_right(self):
return self.move(Direction.RIGHT)
def move_left(self):
return self.move(Direction.LEFT)
def _move_and_integrate(self, direction):
old_parent = self.parent
self.move(direction)
if self.parent is not old_parent:
self.integrate(direction)
def integrate(self, direction):
if direction.orient != self.parent.orient:
self._move_and_integrate(direction)
return
target_idx = self.index + direction.offset
if target_idx < 0 or target_idx >= len(self.parent):
self._move_and_integrate(direction)
return
self.reset_size()
target = self.parent[target_idx]
self.parent.remove_child(self)
if target.is_leaf:
target.flip_with(self)
else:
target.add_child(self)
def integrate_up(self):
self.integrate(Direction.UP)
def integrate_down(self):
self.integrate(Direction.DOWN)
def integrate_left(self):
self.integrate(Direction.LEFT)
def integrate_right(self):
self.integrate(Direction.RIGHT)
def find_payload(self, payload):
if self.payload is payload:
return self
for child in self:
needle = child.find_payload(payload)
if needle is not None:
return needle
return None
[docs]class Plasma(Layout):
"""
A flexible tree-based layout.
Each tree node represents a container whose children are aligned either
horizontally or vertically. Each window is attached to a leaf of the tree
and takes either a calculated relative amount or a custom absolute amount
of space in its parent container. Windows can be resized, rearranged and
integrated into other containers.
Windows in a container will all open in the same direction. Calling
``lazy.layout.mode_vertical/horizontal()`` will insert a new container allowing
windows to be added in the new direction.
You can use the ``Plasma`` widget to show which mode will apply when opening a new
window based on the currently focused node.
Windows can be focused selectively by using ``lazy.layout.up/down/left/right()`` to focus
the nearest window in that direction relative to the currently focused window.
"Integrating" windows is best explained with an illustation. Starting with three
windows, a, b, c. b is currently focused. Calling ``lazy.layout.integrate_left()``
will have the following effect:
::
---------------------- ----------------------
| a | b | c | | a | c |
| | | | | | |
| | | | --> | | |
| | | | |----------| |
| | | | | b | |
| | | | | | |
| | | | | | |
---------------------- ----------------------
Finally, windows can me moved around the layout with ``lazy.layout.move_up/down/left/right()``.
Example keybindings:
.. code:: python
from libqtile.config import EzKey
from libqtile.lazy import lazy
...
keymap = {
'M-h': lazy.layout.left(),
'M-j': lazy.layout.down(),
'M-k': lazy.layout.up(),
'M-l': lazy.layout.right(),
'M-S-h': lazy.layout.move_left(),
'M-S-j': lazy.layout.move_down(),
'M-S-k': lazy.layout.move_up(),
'M-S-l': lazy.layout.move_right(),
'M-A-h': lazy.layout.integrate_left(),
'M-A-j': lazy.layout.integrate_down(),
'M-A-k': lazy.layout.integrate_up(),
'M-A-l': lazy.layout.integrate_right(),
'M-d': lazy.layout.mode_horizontal(),
'M-v': lazy.layout.mode_vertical(),
'M-S-d': lazy.layout.mode_horizontal_split(),
'M-S-v': lazy.layout.mode_vertical_split(),
'M-a': lazy.layout.grow_width(30),
'M-x': lazy.layout.grow_width(-30),
'M-S-a': lazy.layout.grow_height(30),
'M-S-x': lazy.layout.grow_height(-30),
'M-C-5': lazy.layout.size(500),
'M-C-8': lazy.layout.size(800),
'M-n': lazy.layout.reset_size(),
}
keys = [EzKey(k, v) for k, v in keymap.items()]
Acknowledgements:
This layout was developed by numirias and published at
https://github.com/numirias/qtile-plasma A few minor amendments have been made
to that layout as part of incorporating this into the main qtile codebase but the
majority of the work is theirs.
"""
defaults = [
("name", "Plasma", "Layout name"),
("border_normal", "#333333", "Unfocused window border color"),
("border_focus", "#00e891", "Focused window border color"),
("border_normal_fixed", "#333333", "Unfocused fixed-size window border color"),
("border_focus_fixed", "#00e8dc", "Focused fixed-size window border color"),
("border_width", 1, "Border width"),
("border_width_single", 0, "Border width for single window"),
("margin", 0, "Layout margin"),
(
"fair",
False,
"When ``False`` effort will be made to preserve nodes with a fixed size. "
"Set to ``True`` to enable new windows to take more space from fixed size nodes.",
),
]
# If windows are added before configure() was called, the screen size is
# still unknown, so we need to set some arbitrary initial root dimensions
default_dimensions = (0, 0, 1000, 1000)
def __init__(self, **config):
Layout.__init__(self, **config)
self.add_defaults(Plasma.defaults)
self.root = Node(None, *self.default_dimensions)
self._focused = None
self._add_mode = None
Node.priority = Priority.BALANCED if self.fair else Priority.FIXED
def swap(self, c1: Window, c2: Window) -> None:
node_c1 = node_c2 = None
for leaf in self.root.all_leafs:
if leaf.payload is not None:
if c1 == leaf.payload:
node_c1 = leaf
elif c2 == leaf.payload:
node_c2 = leaf
if node_c1 is not None and node_c2 is not None:
node_c1.payload, node_c2.payload = node_c2.payload, node_c1.payload
self.group.layout_all()
self.group.focus(c1)
return
@staticmethod
def convert_names(tree):
return [Plasma.convert_names(n) if isinstance(n, list) else n.payload.name for n in tree]
@property
def add_mode(self):
if self._add_mode is None:
node = self.root_or_focused_node
if node.width >= node.height:
return AddMode.HORIZONTAL
else:
return AddMode.VERTICAL
return self._add_mode
@add_mode.setter
def add_mode(self, value):
self._add_mode = value
# We trigger a redraw so that the different borders can be drawn based on the add_mode
# We check self._group to avoid raising a runtime error from libqtile.layout.base
if self._group is not None:
hook.fire("plasma_add_mode", self)
self.group.layout_all()
@property
def focused(self):
return self._focused
@focused.setter
def focused(self, value):
self._focused = value
hook.fire("plasma_add_mode", self)
@property
def focused_node(self):
return self.root.find_payload(self.focused)
@property
def root_or_focused_node(self):
return self.root if self.focused_node is None else self.focused_node
@property
def horizontal(self):
if self.focused_node is None:
return True
if self.add_mode is not None:
if self.add_mode & AddMode.HORIZONTAL:
return True
else:
return False
if self.focused_node.parent is None:
if self.focused_node.orient is Orient.HORIZONTAL:
return True
else:
return False
return self.focused_node.parent.horizontal
@property
def vertical(self):
return not self.horizontal
@property
def split(self):
if self.add_mode is not None and self.add_mode & AddMode.SPLIT:
return True
return False
[docs] @expose_command
def info(self):
info = super().info()
tree = self.convert_names(self.root.tree)
info["tree"] = tree
info["clients"] = flatten(tree)
return info
def clone(self, group):
clone = copy.copy(self)
clone._group = group
clone.root = Node(None, *self.default_dimensions)
clone.focused = None
clone.add_mode = None
return clone
def get_windows(self):
clients = []
for leaf in self.root.all_leafs:
if leaf.payload is not None:
clients.append(leaf.payload)
return clients
def add_client(self, client):
new = Node(client)
try:
self.root.restore(new)
except NotRestorableError:
self.root_or_focused_node.add_node(new, self.add_mode)
self.add_mode = None
def remove(self, client):
self.root.find_payload(client).remove()
def configure(self, client, screen_rect):
self.root.x = screen_rect.x
self.root.y = screen_rect.y
self.root.width = screen_rect.width
self.root.height = screen_rect.height
node = self.root.find_payload(client)
border_width = self.border_width_single if self.root.tree == [node] else self.border_width
border_color = getattr(
self,
"border_"
+ ("focus" if client.has_focus else "normal")
+ ("" if node.flexible else "_fixed"),
)
x, y, width, height = node.pixel_perfect
client.place(
x,
y,
width - 2 * border_width,
height - 2 * border_width,
border_width,
border_color,
margin=self.margin,
)
# Always keep tiles below floating windows
client.unhide()
def focus(self, client):
self.focused = client
self.root.find_payload(client).access()
def focus_first(self):
return self.root.first_leaf.payload
def focus_last(self):
return self.root.last_leaf.payload
def focus_next(self, win):
next_leaf = self.root.find_payload(win).next_leaf
return None if next_leaf is self.root.first_leaf else next_leaf.payload
def focus_previous(self, win):
prev_leaf = self.root.find_payload(win).prev_leaf
return None if prev_leaf is self.root.last_leaf else prev_leaf.payload
def focus_node(self, node):
if node is None:
return
self.group.focus(node.payload)
def refocus(self):
self.group.focus(self.focused)
[docs] @expose_command
def next(self):
"""Focus next window."""
self.focus_node(self.focused_node.next_leaf)
[docs] @expose_command
def previous(self):
"""Focus previous window."""
self.focus_node(self.focused_node.prev_leaf)
[docs] @expose_command
def recent(self):
"""Focus most recently focused window.
(Toggles between the two latest active windows.)
"""
nodes = [n for n in self.root.all_leafs if n is not self.focused_node]
most_recent = max(nodes, key=lambda n: n.last_accessed)
self.focus_node(most_recent)
[docs] @expose_command
def left(self):
"""Focus window to the left."""
self.focus_node(self.focused_node.close_left)
[docs] @expose_command
def right(self):
"""Focus window to the right."""
self.focus_node(self.focused_node.close_right)
[docs] @expose_command
def up(self):
"""Focus window above."""
self.focus_node(self.focused_node.close_up)
[docs] @expose_command
def down(self):
"""Focus window below."""
self.focus_node(self.focused_node.close_down)
[docs] @expose_command
def move_left(self):
"""Move current window left."""
self.focused_node.move_left()
self.refocus()
[docs] @expose_command
def move_right(self):
"""Move current window right."""
self.focused_node.move_right()
self.refocus()
[docs] @expose_command
def move_up(self):
"""Move current window up."""
self.focused_node.move_up()
self.refocus()
[docs] @expose_command
def move_down(self):
"""Move current window down."""
self.focused_node.move_down()
self.refocus()
[docs] @expose_command
def integrate_left(self):
"""Integrate current window left."""
self.focused_node.integrate_left()
self.refocus()
[docs] @expose_command
def integrate_right(self):
"""Integrate current window right."""
self.focused_node.integrate_right()
self.refocus()
[docs] @expose_command
def integrate_up(self):
"""Integrate current window up."""
self.focused_node.integrate_up()
self.refocus()
[docs] @expose_command
def integrate_down(self):
"""Integrate current window down."""
self.focused_node.integrate_down()
self.refocus()
[docs] @expose_command
def mode_horizontal(self):
"""Next window will be added horizontally."""
self.add_mode = AddMode.HORIZONTAL
[docs] @expose_command
def mode_vertical(self):
"""Next window will be added vertically."""
self.add_mode = AddMode.VERTICAL
[docs] @expose_command
def mode_horizontal_split(self):
"""Next window will be added horizontally, splitting space of current
window.
"""
self.add_mode = AddMode.HORIZONTAL | AddMode.SPLIT
[docs] @expose_command
def mode_vertical_split(self):
"""Next window will be added vertically, splitting space of current
window.
"""
self.add_mode = AddMode.VERTICAL | AddMode.SPLIT
[docs] @expose_command
def set_size(self, x: int):
"""Change size of current window.
(It's recommended to use `width()`/`height()` instead.)
"""
self.focused_node.size = x
self.refocus()
[docs] @expose_command
def set_width(self, x: int):
"""Set width of current window."""
self.focused_node.width = x
self.refocus()
[docs] @expose_command
def set_height(self, x: int):
"""Set height of current window."""
self.focused_node.height = x
self.refocus()
[docs] @expose_command
def reset_size(self):
"""Reset size of current window to automatic (relative) sizing."""
self.focused_node.reset_size()
self.refocus()
[docs] @expose_command
def grow(self, x: int):
"""Grow size of current window.
(It's recommended to use `grow_width()`/`grow_height()` instead.)
"""
self.focused_node.size += x
self.refocus()
[docs] @expose_command
def grow_width(self, x: int):
"""Grow width of current window."""
self.focused_node.width += x
self.refocus()
[docs] @expose_command
def grow_height(self, x: int):
"""Grow height of current window."""
self.focused_node.height += x
self.refocus()